bin/*: Use plain `/usr/bin/tclsh' in shebang lines.
[ca] / bin / check
1 #! /usr/bin/tclsh
2 ### -*-tcl-*-
3 ###
4 ### Check that the certificate authority database and files are consistent.
5 ###
6 ### (c) 2011 Mark Wooding
7 ###
8
9 ###----- Licensing notice ---------------------------------------------------
10 ###
11 ### This program is free software; you can redistribute it and/or modify
12 ### it under the terms of the GNU General Public License as published by
13 ### the Free Software Foundation; either version 2 of the License, or
14 ### (at your option) any later version.
15 ###
16 ### This program is distributed in the hope that it will be useful,
17 ### but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ### GNU General Public License for more details.
20 ###
21 ### You should have received a copy of the GNU General Public License
22 ### along with this program; if not, write to the Free Software Foundation,
23 ### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25 ## Find the common utilities.
26 source [file join [file dirname $argv0] "../lib/func.tcl"]
27
28 ## Open the database
29 sqlite3 db "$CERTROOT/state/ca.db"
30 db nullvalue nil
31
32 ## Build a map of the active requests. Verify that active requests have
33 ## distinct tags.
34 array unset actreq
35 array unset complain
36 foreach {tag id} [db eval {
37 SELECT tag, id FROM request WHERE st = 'active';
38 }] {
39 if {[info exists actreq($tag)] && ![info exists complain(dup-req-$tag)]} {
40 bad 4 "multiple active requests with tag `$tag' ($id and $actreq($tag))"
41 set complain(dup-req-$tag) 1
42 } else {
43 set actreq($tag) $id
44 }
45 }
46
47 ## Go through the active certificates. Each one should tie up to an active
48 ## request. We don't check here that the request exists at all: that gets
49 ## done later.
50 array unset actcert
51 foreach {seq tag st} [db eval {
52 SELECT c.seq, r.tag, r.st
53 FROM certificate AS c JOIN request AS r ON c.req = r.id
54 WHERE c.st = 'active';
55 }] {
56 if {[info exists actcert($tag)] &&
57 ![info exists complain(dup-cert-$tag)]} {
58 bad 4 [join {
59 "multiple active certificates with "
60 "tag `$tag' ($seq and $actcert($tag))"} ""]
61 continue
62 }
63 set actcert($tag) $seq
64 if {![string equal $st active]} {
65 bad 2 [join {
66 "active cert $seq associated with "
67 "request $id (`$tag') which is $st not active"} ""]
68 }
69 }
70
71 ## Check that the certificates for a revoked request are revoked or
72 ## expired.
73 foreach {seq id tag st} [db eval {
74 SELECT c.seq, r.id, r.tag, c.st
75 FROM certificate AS c JOIN request AS r ON c.req = r.id
76 WHERE r.st = 'revoked' AND c.st != 'revoked' AND c.st != 'expired';
77 }] {
78 bad 4 "cert $seq for revoked request $id (`$tag') is $st not revoked"
79 }
80
81 ## Similarly, check that revoked certificates match up with revoked
82 ## requests.
83 foreach {seq id tag st} [db eval {
84 SELECT c.seq, r.id, r.tag, c.st
85 FROM certificate AS c JOIN request AS r ON c.req = r.id
86 WHERE c.st = 'revoked' AND r.st != 'revoked';
87 }] {
88 bad 2 [join {
89 "revoked cert $seq associated with "
90 "request $id (`$tag') which is $st not revoked"} ""]
91 }
92
93 ## Check that the active symlinks are correct.
94 foreach {what key dir actvar} {
95 "request" "id" "req" actreq
96 "certificate" "seq" "cert" actcert
97 } {
98 upvar 0 $actvar act
99
100 ## Check that there's a symlink DIR/active/TAG for each active item, and
101 ## that it points to the correct item.
102 foreach tag [array names act] {
103 set link "$CERTROOT/$dir/active/$tag"
104 set id $act($tag)
105 if {![file exists $link]} {
106 bad 1 "missing symlink for active $what `$tag' ($key = $id)"
107 } elseif {![string equal [file type $link] link]} {
108 bad 1 "entry for active $what `$tag' ($key = $id) isn't a link"
109 } elseif {![string equal [file readlink $link] "../by-$key/$id"]} {
110 bad 1 "link for active $what `$tag' ($key = $id) is wrong"
111 moan "\t(actually `[file readlink $link]'; should be `../by-$key/$id')"
112 }
113 }
114
115 ## Check that there aren't any other stray things.
116 foreach tag \
117 [glob -tails -directory "$CERTROOT/$dir/active" -nocomplain *] {
118 if {![info exists act($tag)]} {
119 bad 1 "bogus file `$dir/active/$tag'"
120 }
121 }
122 }
123
124 ## Now run through all of the requests and check that they match the
125 ## corresponding request files.
126 array unset reqmap
127 foreach {id tag st dn hash} [db eval {
128 SELECT id, tag, st, dn, hash FROM request;
129 }] {
130 if {[info exists reqmap($id)]} {
131 bad 4 "duplicate request id $id"
132 continue
133 }
134 set reqmap($id) 1
135
136 switch -exact -- $st {
137 active - withdrawn - revoked { }
138 default {
139 bad 2 "request $id (`$tag') has unknown state `$st'"
140 }
141 }
142
143 set reqfile "$CERTROOT/req/by-id/$id"
144 if {![file exists $reqfile]} {
145 bad 4 "missing file for request $id (`$tag')"
146 continue
147 }
148
149 set req_dn [req-dn $reqfile]
150 if {![string equal $req_dn $dn]} {
151 bad 2 "request $id (`$tag') has DN mismatch"
152 moan "\t(db has dn = $dn)"
153 moan "\t(file has dn = $req_dn)"
154 }
155
156 set req_hash [req-key-hash $reqfile]
157 if {![string equal $req_hash $hash]} {
158 bad 2 "request $id (`$tag') has key hash mismatch"
159 moan "\t(db has hash = $hash)"
160 moan "\t(file has hash = $req_hash)"
161 }
162 }
163
164 ## Run through all of the certificates and check that they match the
165 ## correspoding certificate files. This is a good opportunity to verify that
166 ## the certificates match up with requests.
167 array unset certmap
168 foreach {seq req tag st dn hash} [db eval {
169 SELECT c.seq, r.id, r.tag, c.st, r.cert_dn, r.hash
170 FROM certificate AS c LEFT OUTER JOIN request AS r ON c.req = r.id;
171 }] {
172 if {[info exists certmap($seq)]} {
173 bad 4 "duplicate certificate serial number $seq"
174 continue
175 }
176 set certmap($seq) 1
177
178 if {[string equal $req nil]} {
179 bad 2 "certificate $seq has no certificate request"
180 }
181
182 switch -exact -- $st {
183 active - withdrawn - superceded - revoked - expired { }
184 default {
185 bad 2 "certificate $id (`$tag') has unknown state `$st'"
186 }
187 }
188
189 set certfile "$CERTROOT/cert/by-seq/$seq"
190 if {![file exists $certfile]} {
191 bad 4 "missing file for certficate $seq (`$tag')"
192 continue
193 }
194 if {[string equal $req nil]} { continue }
195
196 set cert_dn [cert-dn $certfile]
197 if {![string equal $dn $cert_dn]} {
198 bad 2 "certificate $seq (`$tag') has DN mismatch"
199 moan "\t(db has dn = $dn)"
200 moan "\t(file has dn = $cert_dn)"
201 }
202
203 set cert_hash [cert-key-hash $certfile]
204 if {![string equal $cert_hash $hash]} {
205 bad 2 "certificate $seq (`$tag') has key hash mismatch"
206 moan "\t(db has hash = $hash)"
207 moan "\t(file has hash = $cert_hash)"
208 }
209 }
210
211 ## Finally, make sure that there aren't any stray files in those directories.
212 foreach {dir mapvar} {
213 "req/by-id" reqmap
214 "cert/by-seq" certmap
215 } {
216 upvar 0 $mapvar map
217 foreach file [glob -tails -directory "$CERTROOT/$dir" -nocomplain *] {
218 if {![info exists map($file)]} {
219 bad 1 "bogus file `$dir/$file'"
220 }
221 }
222 }
223
224 ## Done!
225 quit
226
227 ###----- That's all, folks --------------------------------------------------