Minimal X.509 certificate authority.
authorMark Wooding <mdw@distorted.org.uk>
Sun, 10 Jul 2011 22:11:40 +0000 (23:11 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Sun, 10 Jul 2011 22:11:40 +0000 (23:11 +0100)
.gitignore [new file with mode: 0644]
bin/issue-crl [new file with mode: 0755]
bin/make-ca-key [new file with mode: 0755]
bin/make-cert [new file with mode: 0755]
bin/refresh [new file with mode: 0755]
lib/func.sh [new file with mode: 0644]
openssl.conf [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..d7e6d72
--- /dev/null
@@ -0,0 +1,6 @@
+ca.cert
+certs
+index
+private
+state
+tmp
diff --git a/bin/issue-crl b/bin/issue-crl
new file mode 100755 (executable)
index 0000000..600e22a
--- /dev/null
@@ -0,0 +1,17 @@
+#! /bin/sh
+
+set -e
+certroot=$(cd ${0%/*}/..; pwd)
+cd "$certroot"
+. lib/func.sh
+runas_ca
+
+now=$(date +%Y-%m-%d)
+n=0
+while t=$now#$n.crl; [ -f crls/$t ]; do
+  n=$(expr $n + 1)
+done
+openssl ca -config openssl.conf -gencrl -out crls/$t
+rm -f crls/new
+ln -s $t crls/new
+mv crls/new crls/current
diff --git a/bin/make-ca-key b/bin/make-ca-key
new file mode 100755 (executable)
index 0000000..f563bd9
--- /dev/null
@@ -0,0 +1,53 @@
+#! /bin/sh
+
+set -e
+certroot=$(cd ${0%/*}/..; pwd)
+cd "$certroot"
+umask 022
+
+## Archive any existing CA.
+if [ -f ca.cert ]; then
+  mkdir -p archive
+  if [ -f archive/state/serial ]; then
+    next=$(cat archive/state/serial)
+  else
+    mkdir -p archive/state
+    next=1
+  fi
+  mkdir archive/"$next"
+  mv ca.cert certs crls index private state archive/"$next"/
+  expr "$next" + 1 >archive/state/serial.new
+  mv archive/state/serial.new archive/state/serial
+fi
+
+## Clear out the old CA completely.
+rm -rf certs index private tmp state
+rm -f ca.cert distorted.crl
+
+## Build a new one.
+mkdir -m750 private
+mkdir -m775 certs crls index index/byhash index/byserial state tmp
+chown root:ca certs crls index index/byhash index/byserial private state tmp
+touch state/db
+echo 01 >state/serial
+echo 01 >state/crlnumber
+
+## Set the CA subject name.  It won't fit on one line, and there's no
+## good way of continuing it.  Have fun parsing the sed.
+subject=$(sed -n 's:^:/:;1h;2,$H;${x;s/\n//g;p;}' <<EOF
+C=GB
+ST=Cambridgeshire
+O=distorted.org.uk
+OU=Certificate Authority
+CN=distorted.org.uk top-level CA
+emailAddress=ca@distorted.org.uk
+EOF
+)
+
+## Build the new CA key and certificate.
+umask 027
+openssl req -new -config openssl.conf -x509 -days 3650 \
+       -out ca.cert -keyout private/ca.key \
+       -subj "$subject"
+chown root:ca private/ca.key
+chmod 644 ca.cert
diff --git a/bin/make-cert b/bin/make-cert
new file mode 100755 (executable)
index 0000000..825f115
--- /dev/null
@@ -0,0 +1,40 @@
+#! /bin/sh
+
+set -e
+certroot=$(cd ${0%/*}/..; pwd)
+. "$certroot"/lib/func.sh
+runas_ca "$@"
+
+## Parse the command line.
+case "$#" in
+  3) ;;
+  *) echo >&2 "Usage: $0 TAG PROFILE FILE"; exit 1 ;;
+esac
+tag=$1 profile=$2 file=$3
+
+## Make sure we're not overwriting anything.  Put sequence numbers
+## into labels to prevent bad things from happening.
+if [ -f "$certroot"/certs/"$tag".cert ]; then
+  echo >&2 "$0: certificate $tag already exists"
+  exit 1
+fi
+
+## Make a temporary copy of the certificate.  This prevents a race, and
+## more importantly lets us change directory.
+cp "$file" "$certroot"/tmp/"$tag".req
+cd "$certroot"
+
+## Make the certificate.
+openssl ca -config openssl.conf -extensions $profile-extensions \
+       -in tmp/"$tag".req -out tmp/"$tag".cert
+
+## Install a hash link the benefit of OpenSSL's `verify' command and
+## similar, and install the completed request and certificate in the
+## archive.
+mv tmp/"$tag".req tmp/"$tag".cert certs/
+linkserial certs/"$tag".cert
+linkhash certs/"$tag".cert
+rm tmp/*.pem
+
+## Output the certificate.
+openssl x509 -in certs/"$tag".cert
diff --git a/bin/refresh b/bin/refresh
new file mode 100755 (executable)
index 0000000..22c444d
--- /dev/null
@@ -0,0 +1,23 @@
+#! /bin/sh
+
+set -e
+certroot=$(cd ${0%/*}/..; pwd)
+cd "$certroot"
+. lib/func.sh
+runas_ca
+
+badness=0
+indices="byhash byserial"
+for i in $indices; do rm -rf index/$i; done
+for i in $indices; do mkdir index/$i.new; done
+
+for i in certs/*.cert; do
+  linkserial "$i" .new
+  linkhash "$i" .new
+done
+
+for i in $indices; do
+  if [ -d index/$i ]; then mv index/$i index/$i.old; fi;
+done
+for i in $indices; do mv index/$i.new index/$i; done
+for i in $indices; do rm -rf index/$i.old; done
diff --git a/lib/func.sh b/lib/func.sh
new file mode 100644 (file)
index 0000000..3cfd55e
--- /dev/null
@@ -0,0 +1,58 @@
+### -*-sh-*-
+
+runas_ca () {
+  ## runas_ca
+  ##
+  ## Make sure we're running as the CA user.  I don't trust ASN.1 parsers
+  ## to run as root against untrusted input -- especially OpenSSL's one.
+
+  case $(id -un) in
+    ca) ;;
+    *) exec sudo -u ca "$0" "$@" ;;
+  esac
+}
+
+linkserial () {
+  ## linkserial CERT [SERIAL]
+  ##
+  ## Make a link for the certificate according to its serial number.
+
+  cert=$1 suffix=$2
+  serial=$(openssl x509 -serial -noout -in "$cert")
+  serial=${serial##*=}
+  t=index/byserial$suffix/$serial.pem
+  if [ -L "$t" ]; then
+    other=$(readlink "$t")
+    echo "Duplicate serial numbers: ${other##*/}, ${cert##*/}"
+    badness=1
+    return
+  fi
+  lns "$cert" "$t"
+}
+
+linkhash () {
+  ## linkhash CERT [SUFFIX]
+  ##
+  ## Make links for the certificate according to its hash.
+
+  cert=$1 suffix=$2
+  fpr=$(openssl x509 -fingerprint -noout -in "$cert")
+  for opt in subject_hash subject_hash_old; do
+    n=0
+    hash=$(openssl x509 -$opt -noout -in "$cert")
+    while t=index/byhash$suffix/$hash.$n; [ -L "$t" ]; do
+      ofpr=$(openssl x509 -fingerprint -noout -in "$t")
+      other=$(readlink "$t")
+      case "${cert##*/}" in "${other##*/}") continue ;; esac
+      case "$ofpr" in
+       "$fpr")
+         echo "Duplicate certificates: ${other##*/}, ${cert##*/}"
+         badness=1
+         return
+         ;;
+      esac
+      n=$(expr $n + 1)
+    done
+    lns "$cert" "$t"
+  done
+}
diff --git a/openssl.conf b/openssl.conf
new file mode 100644 (file)
index 0000000..4ff681e
--- /dev/null
@@ -0,0 +1,114 @@
+### -*-conf-*-
+###
+### OpenSSL configuration for distorted.org.uk CA.
+
+###--------------------------------------------------------------------------
+### Defaults.
+
+RANDFILE = /dev/urandom
+
+###--------------------------------------------------------------------------
+### Certificate request configuration.
+
+[req]
+default_bits = 3072
+encrypt_key = no
+default_md = sha1
+utf8 = yes
+x509_extensions = ca-extensions
+distinguished_name = req-dn
+prompt = yes
+
+[req-dn]
+
+countryName = "Country name"
+countryName_default = "GB"
+countryName_min = 2
+countryName_max = 2
+
+stateOrProvinceName = "State, province, or county"
+stateOrProvinceName_default = "Cambridgeshire"
+stateOrProvinceName_max = 64
+
+localityName = "Locality (e.g., city)"
+localityName_default = "Cambridge"
+localityName_max = 64
+
+organizationName = "Organization"
+organizationName_default = "distorted.org.uk"
+organizationName_max = 64
+organizationalUnitName = "Organizational unit"
+organizationalUnitName_max = 64
+
+commonName = "Common name"
+commonName_max = 64
+
+emailAddress = "Email address"
+emailAddress_max = 64
+
+###--------------------------------------------------------------------------
+### CA configuration.
+
+[ca]
+default_ca = distorted-ca
+preserve = yes
+
+[distorted-ca]
+default_days = 1825
+default_md = sha1
+unique_subject = no
+email_in_dn = no
+private_key = private/ca.key
+certificate = ca.cert
+database = state/db
+serial = state/serial
+crlnumber = state/crlnumber
+default_crl_days = 7
+new_certs_dir = tmp
+x509_extensions = tls-server-extensions
+crl_extensions = crl-extensions
+policy = distorted-policy
+name_opt = sep_multiline, esc_ctrl, utf8, dump_nostr, dump_unknown, space_eq, lname, align
+cert_opt = no_header, ext_parse, no_pubkey
+copy_extensions = copy
+
+[distorted-policy]
+countryName = supplied
+stateOrProvinceName = optional
+localityName = optional
+organizationName = match
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[crl-extensions]
+issuerAltName = email:ca@distorted.org.uk
+crlDistributionPoints=URI:http://www.distorted.org.uk/ca/distorted.crl
+
+[ca-extensions]
+basicConstraints = critical, CA:TRUE
+keyUsage = critical, keyCertSign
+subjectKeyIdentifier = hash
+subjectAltName = email:ca@distorted.org.uk
+crlDistributionPoints=URI:http://www.distorted.org.uk/ca/distorted.crl
+
+[tls-server-extensions]
+basicConstraints = critical, CA:FALSE
+keyUsage = critical, digitalSignature, keyEncipherment
+extendedKeyUsage = serverAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always, issuer:always
+issuerAltName = issuer:copy
+crlDistributionPoints=URI:http://www.distorted.org.uk/ca/distorted.crl
+
+[tls-client-extensions]
+basicConstraints = critical, CA:FALSE
+keyUsage = critical, digitalSignature
+extendedKeyUsage = clientAuth
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+issuerAltName = issuer:copy
+subjectAltName = email:copy
+crlDistributionPoints=URI:http://www.distorted.org.uk/ca/distorted.crl
+
+###----- That's all, folks --------------------------------------------------