Almost a complete rewrite.
[ca] / bin / check
diff --git a/bin/check b/bin/check
new file mode 100755 (executable)
index 0000000..bf276f0
--- /dev/null
+++ b/bin/check
@@ -0,0 +1,227 @@
+#! /usr/bin/tclsh8.5
+### -*-tcl-*-
+###
+### Check that the certificate authority database and files are consistent.
+###
+### (c) 2011 Mark Wooding
+###
+
+###----- Licensing notice ---------------------------------------------------
+###
+### This program is free software; you can redistribute it and/or modify
+### it under the terms of the GNU General Public License as published by
+### the Free Software Foundation; either version 2 of the License, or
+### (at your option) any later version.
+###
+### This program is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+### GNU General Public License for more details.
+###
+### You should have received a copy of the GNU General Public License
+### along with this program; if not, write to the Free Software Foundation,
+### Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+## Find the common utilities.
+source [file join [file dirname $argv0] "../lib/func.tcl"]
+
+## Open the database
+sqlite3 db "$CERTROOT/state/ca.db"
+db nullvalue nil
+
+## Build a map of the active requests.  Verify that active requests have
+## distinct tags.
+array unset actreq
+array unset complain
+foreach {tag id} [db eval {
+  SELECT tag, id FROM request WHERE st = 'active';
+}] {
+  if {[info exists actreq($tag)] && ![info exists complain(dup-req-$tag)]} {
+    bad 4 "multiple active requests with tag `$tag' ($id and $actreq($tag))"
+    set complain(dup-req-$tag) 1
+  } else {
+    set actreq($tag) $id
+  }
+}
+
+## Go through the active certificates.  Each one should tie up to an active
+## request.  We don't check here that the request exists at all: that gets
+## done later.
+array unset actcert
+foreach {seq tag st} [db eval {
+  SELECT c.seq, r.tag, r.st
+  FROM certificate AS c JOIN request AS r ON c.req = r.id
+  WHERE c.st = 'active';
+}] {
+  if {[info exists actcert($tag)] &&
+      ![info exists complain(dup-cert-$tag)]} {
+    bad 4 [join {
+      "multiple active certificates with "
+      "tag `$tag' ($seq and $actcert($tag))"} ""]
+    continue
+  }
+  set actcert($tag) $seq
+  if {![string equal $st active]} {
+    bad 2 [join {
+      "active cert $seq associated with "
+      "request $id (`$tag') which is $st not active"} ""]
+  }
+}
+
+## Check that the certificates for a revoked request are revoked or
+## expired.
+foreach {seq id tag st} [db eval {
+  SELECT c.seq, r.id, r.tag, c.st
+  FROM certificate AS c JOIN request AS r ON c.req = r.id
+  WHERE r.st = 'revoked' AND c.st != 'revoked' AND c.st != 'expired';
+}] {
+  bad 4 "cert $seq for revoked request $id (`$tag') is $st not revoked"
+}
+
+## Similarly, check that revoked certificates match up with revoked
+## requests.
+foreach {seq id tag st} [db eval {
+  SELECT c.seq, r.id, r.tag, c.st
+  FROM certificate AS c JOIN request AS r ON c.req = r.id
+  WHERE c.st = 'revoked' AND r.st != 'revoked';
+}] {
+  bad 2 [join {
+    "revoked cert $seq associated with "
+    "request $id (`$tag') which is $st not revoked"} ""]
+}
+
+## Check that the active symlinks are correct.
+foreach {what key dir actvar} {
+  "request" "id" "req" actreq
+  "certificate" "seq" "cert" actcert
+} {
+  upvar 0 $actvar act
+
+  ## Check that there's a symlink DIR/active/TAG for each active item, and
+  ## that it points to the correct item.
+  foreach tag [array names act] {
+    set link "$CERTROOT/$dir/active/$tag"
+    set id $act($tag)
+    if {![file exists $link]} {
+      bad 1 "missing symlink for active $what `$tag' ($key = $id)"
+    } elseif {![string equal [file type $link] link]} {
+      bad 1 "entry for active $what `$tag' ($key = $id) isn't a link"
+    } elseif {![string equal [file readlink $link] "../by-$key/$id"]} {
+      bad 1 "link for active $what `$tag' ($key = $id) is wrong"
+      moan "\t(actually `[file readlink $link]'; should be `../by-$key/$id')"
+    }
+  }
+
+  ## Check that there aren't any other stray things.
+  foreach tag \
+      [glob -tails -directory "$CERTROOT/$dir/active" -nocomplain *] {
+    if {![info exists act($tag)]} {
+      bad 1 "bogus file `$dir/active/$tag'"
+    }
+  }
+}
+
+## Now run through all of the requests and check that they match the
+## corresponding request files.
+array unset reqmap
+foreach {id tag st dn hash} [db eval {
+  SELECT id, tag, st, dn, hash FROM request;
+}] {
+  if {[info exists reqmap($id)]} {
+    bad 4 "duplicate request id $id"
+    continue
+  }
+  set reqmap($id) 1
+
+  switch -exact -- $st {
+    active - withdrawn - revoked { }
+    default {
+      bad 2 "request $id (`$tag') has unknown state `$st'"
+    }
+  }
+
+  set reqfile "$CERTROOT/req/by-id/$id"
+  if {![file exists $reqfile]} {
+    bad 4 "missing file for request $id (`$tag')"
+    continue
+  }
+
+  set req_dn [req-dn $reqfile]
+  if {![string equal $req_dn $dn]} {
+    bad 2 "request $id (`$tag') has DN mismatch"
+    moan "\t(db has dn = $dn)"
+    moan "\t(file has dn = $req_dn)"
+  }
+
+  set req_hash [req-key-hash $reqfile]
+  if {![string equal $req_hash $hash]} {
+    bad 2 "request $id (`$tag') has key hash mismatch"
+    moan "\t(db has hash = $hash)"
+    moan "\t(file has hash = $req_hash)"
+  }
+}
+
+## Run through all of the certificates and check that they match the
+## correspoding certificate files.  This is a good opportunity to verify that
+## the certificates match up with requests.
+array unset certmap
+foreach {seq req tag st dn hash} [db eval {
+  SELECT c.seq, r.id, r.tag, c.st, r.cert_dn, r.hash
+  FROM certificate AS c LEFT OUTER JOIN request AS r ON c.req = r.id;
+}] {
+  if {[info exists certmap($seq)]} {
+    bad 4 "duplicate certificate serial number $seq"
+    continue
+  }
+  set certmap($seq) 1
+
+  if {[string equal $req nil]} {
+    bad 2 "certificate $seq has no certificate request"
+  }
+
+  switch -exact -- $st {
+    active - withdrawn - superceded - revoked - expired { }
+    default {
+      bad 2 "certificate $id (`$tag') has unknown state `$st'"
+    }
+  }
+
+  set certfile "$CERTROOT/cert/by-seq/$seq"
+  if {![file exists $certfile]} {
+    bad 4 "missing file for certficate $seq (`$tag')"
+    continue
+  }
+  if {[string equal $req nil]} { continue }
+
+  set cert_dn [cert-dn $certfile]
+  if {![string equal $dn $cert_dn]} {
+    bad 2 "certificate $seq (`$tag') has DN mismatch"
+    moan "\t(db has dn = $dn)"
+    moan "\t(file has dn = $cert_dn)"
+  }
+
+  set cert_hash [cert-key-hash $certfile]
+  if {![string equal $cert_hash $hash]} {
+    bad 2 "certificate $seq (`$tag') has key hash mismatch"
+    moan "\t(db has hash = $hash)"
+    moan "\t(file has hash = $cert_hash)"
+  }
+}
+
+## Finally, make sure that there aren't any stray files in those directories.
+foreach {dir mapvar} {
+  "req/by-id" reqmap
+  "cert/by-seq" certmap
+} {
+  upvar 0 $mapvar map
+  foreach file [glob -tails -directory "$CERTROOT/$dir" -nocomplain *] {
+    if {![info exists map($file)]} {
+      bad 1 "bogus file `$dir/$file'"
+    }
+  }
+}
+
+## Done!
+quit
+
+###----- That's all, folks --------------------------------------------------