This post is about deploying custom certificate authority (CA) certificates onto Linux hosts, from an anonymous Windows share, then deploying them to be used by web-browsers (which seem to use their own CA stores these days). There are two scripts, one for each of these tasks, as installing to the system store usually requires super-user (i.e. root) access but installing to the browser stores is per-user (and should not be done using the super-user account).

Installing CA to the system store

This script does make several assumptions and, at present, needs to be configured using in-line variables CERT_DIR, CERT_PREFIX and DEBUG - these should be made command-line options.

Key assumptions are:

  • Certificates are all in the same directory, directly specified as the download source.
  • There is no need to remove previously installed certificates that are no longer in the share.
  • The share allows anonymous access to fetch the certificates.
#!/bin/bash

# Standard bash safety - fail on error, do not allow unintended
# globbing, do not allow uninitialised variables, errors in pipes
# cause whole pipe to fail.
set -efuo pipefail

# TODO: Make these command line options
# UNC path to fetch the certificates from - should be a directory
# containing one or more certifictes to be installed.
CERT_DIR="//isolinear/software/certs/my super ca"
# This will be used as a directory (Debian-like) or filename prefix
# (Red Hat-like) when installing the certificates.
CERT_PREFIX="LOCAL-SUPER-CA"
DEBUG=true

debug() {
  [[ $DEBUG = true ]] && echo "$@" >&2
}

if [[ $UID -eq 0 ]]
then
  USE_SUDO=false
elif sudo -n -l &>/dev/null
then
  # N.B. being able to list sudo just means we can run sudo
  # passwordless (e.g. already authenticated or have nopasswd
  # commands available). It does not mean we can run the specific
  # commands required later - the script may still error.
  USE_SUDO=true
else
  echo "This script needs to be run as root, or be able to sudo," \
    "to install the certificates." >&2
  exit 1
fi

do_su() {
  if [[ ${USE_SUDO} = "true" ]]
  then
    sudo "$@"
  else
    "$@"
  fi
}

echo "Fetching certificates from ${CERT_DIR}..."
# Split into //host/share and directory
regex='^(//[^/]+/[^/]+)(/.*)?$'
if [[ ${CERT_DIR} =~ $regex ]]
then
  # First group
  host_share="${BASH_REMATCH[1]}"
  # Is there a 2nd group, or is it the empty string (didn't match)?
  if [[ -z "${BASH_REMATCH[2]}" ]]
  then
    # Presume root of share, if no directory
    share_dir='/'
  else
    share_dir="${BASH_REMATCH[2]}"
  fi
  debug "Calculated host and share name as: ${host_share}"
  debug "Calculated directory as: ${share_dir}"
  # Work out how many slashes are in the directory name - we will
  # need to strip these off.
  dir_components="${share_dir//[^\/]}"
  dir_component_count=${#dir_components}
  debug "Will strip ${dir_component_count}+1 components."

  # Somewhere to save the files. Make sure this gets tidied up
  # (deleted) later on.
  download_dir=$( mktemp -d )
  download_tar=$( mktemp )

  # Get smbclient to tar the directory
  # This was originally done without a temporary tar file, directly piping
  # smbclient to tar but tar doesn't handle SIGPIPE correctly so that caused
  # a failure with '-o pipefail'.
  # See https://stackoverflow.com/questions/769564/error-code-141-with-tar
  debug "Downloading files to tar file at ${download_tar}..."
  smbclient "${host_share}" -N -d0 -D "${share_dir}" -Tc "${download_tar}"
  debug "Extracting downloaded files into ${download_dir}..."
  # Extract into the download_dir, stripping off the directory name.
  tar -xC "${download_dir}" -f "${download_tar}" \
    --strip-components=$(( ${dir_component_count} + 1 )) 

  # Actually install the certificates - where they go and what command to
  # use to update the system store depends on if this is a Debain or Red
  # Hat like distribution (others are available, only support those two
  # currently).
  if [ -f /etc/debian_version ]
  then
    debug "Debian-like distribution, installing to" \
      "/usr/local/share/ca-certificates and using update-ca-certificates"
    
    target_dir="/usr/local/share/ca-certificates/${CERT_PREFIX}"

    # Check the install directory exists
    if ! [ -e "${target_dir}" ]
    then
      debug "Creating target ${target_dir}..."
      # /usr/local/share/ca-certificates should already exist (and be
      # empty on a new install) so '-p' is unnecessary.
      do_su mkdir "${target_dir}"
      # Certificate files should be public
      do_su chmod 755 "${target_dir}"
    else
      # Don't currently handle deleting old/defunct certificate files
      debug "${target_dir} already exists - not cleaning out any old files."
    fi

    # Install the certificates
    set +f # Need to glob
    for file in "${download_dir}"/*
    do
      set -f # Done globbing
      # Only copy plain files - this presumes all certificates are in
      # the top level directory but saves us from worrying about
      # clashing file names in different directories in the Windows share.
      if [ -f "${file}" ]
      then
        debug "Installing $( basename "${file}" )..."
        # Does the source file end with '.crt' (required for
        # update-ca-certificates)? If not, add it.
        if [[ "${file}" = "*.crt" ]]
        then
          target_file="$( basename "${file}" )"
        else
          # Strip existing extension
          target_file="$( basename "${file%.*}" ).crt"
        fi
        debug "Will copy to destination filename ${target_file}."
        # Check permissions are what we want on source
        do_su chmod 444 "${file}"
        # Copy, preserving permissions and timestamps (default also
        # preserves owner but I _want_ it to be root).
        do_su cp --preserve=mode,timestamps "${file}" \
          "${target_dir}/${target_file}"
      fi
    done
    # In case the loop didn't get entered
    set -f # Done globbing

    # Update the system certificates
    debug "Running update-ca-certificates to add new certs"
    do_su update-ca-certificates
  elif [ -f /etc/redhat-release ]
  then
    debug "Red Hat-like distribution, installing to" \
      "/etc/pki/ca-trust/source/anchors and using update-ca-trust"
    
    # Install the certificates
    set +f # Need to glob
    for file in "${download_dir}"/*
    do
      set -f # Done globbing
      # Only copy plain files - this presumes all certificates are in
      # the top level directory but saves us from worrying about
      # clashing file names in different directories in the Windows share.
      if [ -f "${file}" ]
      then
        debug "Installing $( basename "${file}" )..."
        # Check permissions are what we want on source
        do_su chmod 444 "${file}"
        target="/etc/pki/ca-trust/source/anchors/${CERT_PREFIX}-$( basename "${file}" )"
        [ -e "${target}" ] && echo "WARNING: Overwriting ${target}" >&2
        # Copy, preserving permissions and timestamps (default also
        # preserves owner but I _want_ it to be root).
        do_su cp -f --preserve=mode,timestamps "${file}" "${target}"
      fi
    done
    # In case the loop didn't get entered
    set -f # Done globbing

    # Update the system certificates
    debug "Running update-ca-trust to add new certs"
    do_su update-ca-trust
  else
    echo "Unknown distribution - unable to install certificates." >&2
    exit_state=1
  fi

  # Tidy up
  debug "Deleting ${download_dir}"
  rm -rf "${download_dir}"
  debug "Deleting ${download_tar}"
  rm -f "${download_tar}"
else
  echo "Unable to separate path into //host/share and directory parts." >&2
  exit 1
fi

# Exit with exit_state or zero (0) if none given.
exit ${exit_state:-0}

Adding custom certificates to a user’s web-browser store.

#!/bin/bash

# Standard bash safety - fail on error, do not allow unintended
# globbing, do not allow uninitialised variables, errors in pipes
# cause whole pipe to fail.
set -efuo pipefail

# This will be used to identify the certificates to add, from the
# system store.
CERT_PREFIX="LOCAL-SUPER-CA"
DEBUG=true

debug() {
  [[ $DEBUG = true ]] && echo "$@" >&2
}

if [[ $UID -eq 0 ]]
then
  # Not to mention the question "why are you running a web-browser as root?"
  echo "This script should not be run as root - adding certificates" \
    "to root's web browser is a bad idea." >&2
  exit 1
fi

if ! command -v certutil &>/dev/null
then
  echo "certutil command not found - perhaps it needs installing" \
    "(libnss3-tools on Debian)?" >&2
  exit 1
fi

# Based on https://superuser.com/a/1717924
add_certificate() {
  debug "Adding $1 to older browsers..."
  for cert_db in $(find ~ -name cert8.db)
  do
    debug "Adding to ${cert_db}..."
    # Use base filename sans extension as name
    certutil -A -n "$( basename "${1%.*}" )" -t "TCu,Cu,Tc" -i "$1" \
      -d "dbm:$( dirname "${cert_db}" )"
  done
  debug "Adding $1 to newer browsers..."
  for cert_db in $(find ~ -name cert9.db)
  do
    debug "Adding to ${cert_db}..."
    # Use base filename sans extension as name
    certutil -A -n "$( basename "${1%.*}" )" -t "TCu,Cu,Tc" -i "$1" \
      -d "sql:$( dirname "${cert_db}" )"
  done
}

# Like installing the certificates, where they are depends on the OS
if [ -f /etc/debian_version ]
then
  set +f  # Enable globbing
  shopt -s nullglob  # No matching files is fine
  for file in "/usr/local/share/ca-certificates/${CERT_PREFIX}"/*
  do
    set -f  # Done globbing
    shopt -u nullglob  # Restore default (no null globbing)
    debug "Adding file ${file}..."
    add_certificate "${file}"
  done
  # These are in case the loop didn't get entered
  set -f  # Done globbing
  shopt -u nullglob  # Restore default (no null globbing)
elif [ -f /etc/redhat-release ]
then
  set +f  # Enable globbing
  shopt -s nullglob  # No matching files is fine
  for file in "/etc/pki/ca-trust/source/anchors/${CERT_PREFIX}-"*
  do
    set -f  # Done globbing
    shopt -u nullglob  # Restore default (no null globbing)
    debug "Adding file ${file}..."
    add_certificate "${file}"
  done
  # These are in case the loop didn't get entered
  set -f  # Done globbing
  shopt -u nullglob  # Restore default (no null globbing)
else
  echo "Unknown distribution - unable to install certificates." >&2
  exit 1
fi