Make user https push certificates revokable
[girocco.git] / cgi / usercert.cgi
blob929dfe9e2ac4b630f8a27a44e3368ea94f0937b9
1 #!/usr/bin/perl
3 # usercert.cgi -- user push authentication certificate retrieval
4 # Copyright (c) 2013 Kyle J. McKay. All rights reserved.
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 # Version 1.1
22 use strict;
23 use warnings;
25 use File::Spec;
27 use lib ".";
28 use Girocco::CGI;
29 use Girocco::Config;
30 use Girocco::User;
31 use Girocco::Util;
32 use Girocco::SSHUtil;
34 # returns system result as ((result >> 8) & 0xFF) | ((result & 0xFF) << 8)
35 # so the commands standard exit status will be in the low 8 bits whereas the
36 # signal number (if any) and dumped core bit (if set) will be in the high 8 bits
37 # this makes it somewhat easier to use. A return result of 0 still indicates
38 # success.
39 sub system_quiet {
40 open(NULL, ">", File::Spec->devnull) or die "Cannot open devnull: $!";
41 open(SAVEOUT, ">&STDOUT") || die "couldn't dup STDOUT: $!";
42 open(SAVEERR, ">&STDERR") || die "couldn't dup STDERR: $!";
43 open(STDOUT, ">&NULL") || die "couldn't dup NULL to STDOUT: $!";
44 open(STDERR, ">&NULL") || die "couldn't dup NULL to STDERR: $!";
45 my $result = system @_;
46 $result = (($result >> 8) & 0xFF) | (($result & 0xFF) << 8);
47 open(STDERR, ">&SAVEERR") || die "couldn't dup SAVERR to STDERR: $!";
48 open(STDOUT, ">&SAVEOUT") || die "couldn't dup SAVOUT to STDOUT: $!";
49 close(SAVEERR) or die "couldn't close SAVEERR: $!";
50 close(SAVEOUT) or die "couldn't close SAVEOUT: $!";
51 close(NULL) or die "couldn't close NULL: $!";
52 return $result;
55 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
56 sub cmd_pipe {
57 open(NULL, ">", File::Spec->devnull) or die "Cannot open devnull: $!";
58 open(SAVEERR, ">&STDERR") || die "couldn't dup STDERR: $!";
59 open(STDERR, ">&NULL") || die "couldn't dup NULL to STDERR: $!";
60 my $result = open(my $fd, "-|", @_);
61 open(STDERR, ">&SAVEERR") || die "couldn't dup SAVERR to STDERR: $!";
62 close(SAVEERR) or die "couldn't close SAVEERR: $!";
63 close(NULL) or die "couldn't close NULL: $!";
64 return $result ? $fd : undef;
67 sub notfound
69 my $mesg = shift || "<p>404 - Not found</p>";
70 my $gcgi = Girocco::CGI->new('User HTTPS Push Authentication Certificate');
71 print $mesg;
72 exit;
75 sub mtime
77 my $fname = shift;
78 my (undef,undef,undef,undef,undef,undef,undef,undef,undef,$mtime) = stat $fname;
79 return $mtime;
82 sub readfile
84 my $fname = shift;
85 local $/;
86 open(FILE, '<', $fname) or return undef;
87 my $contents = <FILE>;
88 close(FILE);
89 return $contents;
92 notfound unless $Girocco::Config::clientcert && $Girocco::Config::clientkey;
93 notfound unless -f $Girocco::Config::clientcert && -f $Girocco::Config::clientkey;
94 notfound unless -r $Girocco::Config::clientcert && -r $Girocco::Config::clientkey;
96 my $cgi = CGI->new;
98 notfound if $cgi->request_method() ne 'GET' && $cgi->request_method() ne 'HEAD';
99 notfound "<p>Go away, bot.</p>" if $cgi->param('mail');
101 my $nick = $Girocco::Config::nickname;
103 my $name = $cgi->param('name');
104 $name =~ s/^\s*(.*?)\s*$/$1/ if $name;
105 my $line = $cgi->param('line');
106 $line =~ s/^\s*(.*?)\s*$/$1/ if $line;
107 my $view = $cgi->param('view');
108 $view =~ s/^\s*(.*?)\s*$/$1/ if $view;
110 notfound if $cgi->path_info() && ($name || $line);
112 if ($cgi->path_info() =~ m,/([^/]+)/([^/]+)/([^/]+)$,) {
113 my ($tn,$tl,$tp) = ($1,$2,$3);
114 if ("${nick}_${tn}_user_$tl.pem" eq $tp) {
115 $name = $tn;
116 $line = $tl;
120 notfound unless $name && $line && $line =~ /^[1-9][0-9]*$/;
121 $line += 0;
123 notfound unless Girocco::User::valid_name($name) && Girocco::User::does_exist($name);
124 my $user = Girocco::User->load($name) or notfound;
126 my @keys = split(/\r?\n/, $user->{keys});
127 $line <= @keys or notfound;
128 my ($type, $bits, $fingerprint, $comment) = sshpub_validate($keys[$line-1]);
129 $type && $type eq 'ssh-rsa' or notfound;
131 my $usercakey = jailed_file("/etc/sshcerts/${nick}_${name}_cakey.pem");
132 my $userca = jailed_file("/etc/sshcerts/${nick}_${name}_ca.pem");
133 if (! -f $usercakey || ! -r $usercakey) {
134 unlink $usercakey;
135 unlink $userca;
136 ! -e $usercakey && ! -e $userca or notfound;
137 system_quiet('openssl', 'genrsa', '-f4', '-out', $usercakey, '2048') == 0
138 or notfound;
139 chmod 0660, $usercakey;
140 -f $usercakey && -r $usercakey or notfound;
142 if (! -f $userca || ! -r $userca) {
143 unlink $userca;
144 ! -e $userca && -e $usercakey or notfound;
145 -f $usercakey && -r $usercakey or notfound;
146 defined(my $fd = cmd_pipe 'openssl', 'rsa', '-in', $usercakey, '-pubout')
147 or notfound;
148 my $pubkey = join '', <$fd>;
149 close $fd;
150 my @args = (
151 "$Girocco::Config::basedir/bin/CACreateCert", "--quiet", "--subca",
152 "--cert", $Girocco::Config::clientcert,
153 "--key", $Girocco::Config::clientkey,
154 "--out", $userca,
155 "$user->{name} user certificate authority $user->{uuid}"
157 open PIPE, "|-", @args or notfound;
158 print PIPE $pubkey, "\n";
159 close PIPE or notfound;
160 chmod 0664, $userca;
161 -f $userca && -r $userca or notfound;
164 my $sshkeys = jailed_file($user->_sshkey_path);
165 my $kname = "${nick}_${name}_user_$line.pem";
166 my $sshcert = jailed_file("/etc/sshcerts/$kname");
167 my $sshkeysmtime = mtime $sshkeys;
168 notfound unless $sshkeysmtime;
169 my $sshcertmtime = mtime $sshcert;
170 if (!$sshcertmtime || $sshkeysmtime >= $sshcertmtime) {
171 unlink $sshcert;
172 my @args = (
173 "$Girocco::Config::basedir/bin/CACreateCert", "--quiet", "--client",
174 "--cert", $userca,
175 "--key", $usercakey,
176 "--out", $sshcert,
177 "--suffix", $userca
179 push @args, "--suffix", $Girocco::Config::clientcertsuffix
180 if $Girocco::Config::clientcertsuffix;
181 push @args, $name;
182 open PIPE, "|-", @args or notfound;
183 print PIPE $keys[$line-1], "\n";
184 close PIPE or notfound;
185 chmod 0664, $sshcert;
187 notfound unless -r $sshcert;
188 my $certdata = readfile $sshcert;
189 notfound unless $certdata;
190 my $isviewonly = 0;
191 $isviewonly = 0 + $view if $view && $view =~ /^[01]$/;
192 if ($isviewonly) {
193 print "Content-Type: text/plain\r\n"
194 } else {
195 print "Content-Type: application/octet-stream\r\n";
196 print "Content-Disposition: attachment; filename=\"$kname\"\r\n";
198 print "\r\n";
199 print $certdata unless $cgi->request_method() eq 'HEAD';
201 exit 0;