diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..78a6ad6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +clrtrust-helper +clrtrust diff --git a/Makefile b/Makefile index b976ebd..dc74996 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,27 @@ # Copyright 2017 Intel Corporation. -all: - @true + +LDLIBS := -lcrypto +CFLAGS := -W -Wall -Werror -std=gnu9x + +BINDIR := /usr/bin +LIBEXECDIR := /usr/libexec + +.PHONY: build install check clean + +build: clrtrust-helper clrtrust + +clrtrust-helper: clrtrust-helper.o + +clrtrust: clrtrust.in + cat clrtrust.in | sed -e 's:LIBEXEC_CONFIG_VALUE:$(LIBEXECDIR):' > $@ + chmod +x clrtrust install: - install -D --mode=0755 clrtrust ${INSTALL_ROOT}/usr/bin/clrtrust + install -D --mode=0755 clrtrust ${INSTALL_ROOT}${BINDIR}/clrtrust + install -D --mode=0755 clrtrust-helper ${INSTALL_ROOT}${LIBEXECDIR}/clrtrust-helper -check: +check: build bats -t test +clean: + rm -rf clrtrust-helper clrtrust-helper.o clrtrust diff --git a/clrtrust-helper.c b/clrtrust-helper.c new file mode 100644 index 0000000..dcf2158 --- /dev/null +++ b/clrtrust-helper.c @@ -0,0 +1,164 @@ +/* + * Copyright © 2018 Intel Corporation. + * + * See COPYING for terms. + */ + +/* This is a helper application which is meant to be used with clrtrust, the + * Clear Linux Trust Store management tool. It is created for performance + * reasons, to process certificates in bulk as opposed to running openssl for + * each certificate file. + * + * It reads the list of files, one filename per line, from the standard input + * and produces output in form of: \t. + * + * Two modes are supported. If '-f' switch is specified on the command line, for + * each file a SHA-1 fingerprint is calculated. If '-s' is specified, then + * subject hash is produced. + */ + +#include +#include +#include +#include +#include +#include + +/* process return codes */ +#define CTH_EOK 0 /* success. */ +#define CTH_EERR 1 /* incorrect invocation, cannot start. */ +#define CTH_EINV 2 /* invalid input, cannot produce result. */ + +typedef enum { + MODE_FINGER = 1, /* output fingerprint */ + MODE_HASH, /* output subject hash */ + MODE_INVALID = 100 +} runmode_t; + +void print_help() { + char *help = "clrtrust-helper [-f|-s]\n" + "Helper utility for clrtrust. Reads the list of files from" + " stdin, one per line, and produces either fingerprint or" + " subject hash for each file on stdout in form:\n" + " \n"; + puts(help); +} + +int main(int argc, char **argv) { + + runmode_t runmode = MODE_INVALID; /* global switch: whether to produce fingerprint + or subject hash. */ + + BIO *inbio = NULL; + + /* fingerprint-related variables */ + const EVP_MD *finger_type = NULL; + unsigned int finger_sz; + unsigned char finger[EVP_MAX_MD_SIZE]; + + /* subjecthash-related variables */ + unsigned long subject_hash; + + unsigned int i; + char c; + + char *fname = NULL; + size_t sz = 0; + + /* return code */ + int ret = CTH_EOK; + + while ((c = getopt(argc, argv, "fs")) != -1) { + switch (c) { + case 'f': + if (runmode == MODE_INVALID) { + runmode = MODE_FINGER; + } else { + print_help(); + return CTH_EERR; + } + break; + case 's': + if (runmode == MODE_INVALID) { + runmode = MODE_HASH; + } else { + print_help(); + return CTH_EERR; + } + break; + default: + printf("Unrecognized option -%c.\n", c); + print_help(); + return CTH_EERR; + } + } + + if (runmode == MODE_INVALID) { + print_help(); + return CTH_EERR; + } + + OpenSSL_add_all_algorithms(); + + finger_type = EVP_sha1(); + + inbio = BIO_new(BIO_s_file()); + + while (getline(&fname, &sz, stdin) != -1) { + int fname_len = strlen(fname); + int err = 1; + FILE *fp = NULL; + X509 *cert = NULL; + + if (fname_len < 1) continue; + if (fname[fname_len-1] == '\n') { + fname[fname_len-1] = '\0'; + } + + if (!(fp = fopen(fname, "r"))) { + goto wrap_up; + } + + if (!BIO_set_fp(inbio, fp, BIO_NOCLOSE)) { + goto wrap_up; + } + + if (!(cert = PEM_read_bio_X509(inbio, NULL, 0, NULL))) { + goto wrap_up; + } + + switch (runmode) { + case MODE_FINGER: + if (!X509_digest(cert, finger_type, finger, &finger_sz)) { + goto wrap_up; + } + printf("%s\t", fname); + for (i=0; i < finger_sz; i++) { + printf("%02X", finger[i]); + if (i < finger_sz-1) printf(":"); + } + putc('\n', stdout); + err = 0; + break; + case MODE_HASH: + subject_hash = X509_subject_name_hash(cert); + printf("%s\t%08lx\n", fname, subject_hash); + err = 0; + break; + default: + return CTH_EERR; + } + +wrap_up: + if (fp) fclose(fp); + if (cert) X509_free(cert); + if (err) { + printf("%s\tERROR\n", fname); + ret = CTH_EINV; + } + } + + if (fname) free(fname); + + return ret; +} diff --git a/clrtrust b/clrtrust.in similarity index 90% rename from clrtrust rename to clrtrust.in index 5530306..b335cfc 100755 --- a/clrtrust +++ b/clrtrust.in @@ -108,6 +108,29 @@ EINVAL=22 # invalid argument EBADST=127 # invalid state / health check does not pass EERR=255 # general error +##### LOCATION OF THE HELPER +# always favor env override. if not, prefer the helper that is found next to the +# script itself (it indicates the development environment). if not, look at the +# configured location. +LIBEXEC_DIR=LIBEXEC_CONFIG_VALUE +CLR_HELPER_NAME=clrtrust-helper +for helper_dir in ${CLR_LIBEXEC_DIR:+"${CLR_LIBEXEC_DIR}"} "$(dirname $0)" "${LIBEXEC_DIR}"; do + if [ -x "${helper_dir}/${CLR_HELPER_NAME}" ]; then + CLR_HELPER_CMD="${helper_dir}/${CLR_HELPER_NAME}" + break + fi +done + +if [ ! -x "$CLR_HELPER_CMD" ]; then + 1>&2 echo "Cannot locate the helper executable (${CLR_HELPER_NAME})" + exit $EBADST +fi + +# get absolute path to the helper (to be able to run in subshells) +if [[ "${CLR_HELPER_CMD}" != /* ]]; then + CLR_HELPER_CMD=$(realpath "${CLR_HELPER_CMD}") +fi + ##### LOCK FILE PATH # absolute dirs where we can have lock, must be writeable. potentially, the # choice of lock directory could differ between two parallel runs of clrtrust, @@ -135,21 +158,34 @@ find_certs() { local dir=$1 if [ -z $dir ]; then return 255; fi if [ ! -d $dir ]; then return 255; fi - find $dir -maxdepth 1 -type f | while read f; do - finger=$(openssl x509 -in "${f}" -noout -fingerprint -sha1 2>/dev/null) - if [ $? -ne 0 ]; then - 1>&2 echo "WARNING: file ${f} is not a certificate" - continue - fi - finger=${finger#SHA1 Fingerprint=} - printf "%s\t%s\n" "${f}" "${finger}" - done - return 0 + find $dir -maxdepth 1 -type f | "${CLR_HELPER_CMD}" -f + + # NB: CLR_HELPER_CMD will return 2 exit status if some of the certificates + # cannot be opened/processed. calls to find_certs rely on this fact + # throughout this script. } find_all_certs() { + local ret + local nret find_certs $CLR_CLEAR_TRUST_SRC/trusted + ret=$? find_certs $CLR_LOCAL_TRUST_SRC/trusted + nret=$? + if [ $nret -ne 0 ]; then + ret=$nret + fi + return $ret +} + +filter_bad_certs() { + local bad_cert_files=$(echo "$1" | sed -ne '/\tERROR$/ { s/\tERROR$//; p }') + if [ -n "$bad_cert_files" ]; then + for bad_file in "$bad_cert_files"; do + 1>&2 echo "$bad_file is not a PEM-encoded X.509 certificate. Skipping..." + done + fi + echo "$1" | sed -e '/\tERROR$/d' } # a c_rehash implementation for creating openssl-style CApath. this @@ -166,14 +202,20 @@ c_rehash_internal() { fi ( cd "$dir" - find . -maxdepth 1 -type f | while read f; do - name=$(openssl x509 -in "$f" -noout -subject_hash 2>/dev/null) + hashes=$(find . -maxdepth 1 -type f | "${CLR_HELPER_CMD}" -s) + if [ $? -ne 0 ]; then + return 1 + fi + echo "$hashes" | while IFS=$'\t' read f h; do lnno=0 - while ! ln -s "$f" "${name}.${lnno}" && ((lnno++ < 100)); do + while ! ln -s "$f" "${h}.${lnno}" && ((lnno++ < 100)); do : done 2>/dev/null done ) + if [ $? -ne 0 ]; then + return 1 + fi return 0 } @@ -338,7 +380,7 @@ cmd_generate() { local dup_certs_cnt local distrust_certs_cnt local tmp - local ret + local ret=0 local opt_skip_check # this option is used when called internally after the # a check has been performed local opt_force=0 @@ -375,6 +417,10 @@ cmd_generate() { # find all the certificates ca_certs=$(find_all_certs) + if [ $? -eq 2 ]; then + ca_certs=$(filter_bad_certs "$ca_certs") + ret=$EBADST + fi if [ -z "${ca_certs}" ] && is_system_store && [ $opt_force -ne 1 ]; then 1>&2 cat <$tmp) if [ $? -ne 0 ]; then - 1>&2 echo "$f is not an X.509 certificate. Skipping..." + 1>&2 echo "$f is not a PEM-encoded X.509 certificate. Skipping..." cat $tmp continue fi @@ -732,6 +790,10 @@ $1" files=$(cat $out) ids=$(echo $ids && (echo "$files" | cut -f 2)) certs=$(find_all_certs) + if [ $? -eq 2 ]; then + certs=$(filter_bad_certs "$certs") + ret=$EINVAL + fi for id in $ids; do f=$(echo "$certs" | grep $id) if [ $? -eq 0 ]; then diff --git a/test/copy-negative.bats b/test/copy-negative.bats new file mode 100755 index 0000000..a3f623a --- /dev/null +++ b/test/copy-negative.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats +# Copyright 2017 Intel Corporation + +load test_lib + +setup() { + find_clrtrust + setup_fs + # copy bad certificate + cp $CERTS/bad/non-cert.txt $CLR_LOCAL_TRUST_SRC/trusted + # copy good certificate + cp $CERTS/c1.pem $CLR_LOCAL_TRUST_SRC/trusted +} + +@test "helper returns error code 2 if bad certificate found" { + find $CLR_LOCAL_TRUST_SRC/trusted -type f | { + run $CLRTRUST_HELPER -f + echo $output + # should return 2 + [ $status -eq 2 ] + } +} + +@test "generate returns error code 127 if bad certificate found" { + run $CLRTRUST generate + # should return 127 + echo "$output" | grep "is not a PEM-encoded X.509" + [ $status -eq 127 ] +} + +teardown() { + remove_fs +} + +# vim: ft=sh:sw=4:ts=4:et:tw=80:si:noai:nocin diff --git a/test/test_lib.bash b/test/test_lib.bash index c6eaf21..7d8f180 100644 --- a/test/test_lib.bash +++ b/test/test_lib.bash @@ -6,6 +6,11 @@ find_clrtrust() { else return 1 fi + if [ -x $BATS_TEST_DIRNAME/../clrtrust-helper ]; then + CLRTRUST_HELPER=$(realpath $BATS_TEST_DIRNAME/../clrtrust-helper) + else + return 1 + fi } setup_fs() {