#!/bin/bash # # RFC3161 and RFC5816 Timestamping for git repositories. # # Copyright (c) 2021 Mabulous GmbH # Authors: Matthias Bühlmann # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # # The interactive user interfaces in modified source and object code versions # of this program must display Appropriate Legal Notices, as required under # Section 5 of the GNU Affero General Public License version 3. In accordance # with Section 7(b) of the GNU Affero General Public License, you must retain # the Info line in every timestamp that is created or manipulated using a # covered work. # # You can be released from the requirements of the license by purchasing # a commercial license. Buying such a license is mandatory as soon as you # develop commercial activities involving this software without # disclosing the source code of your own applications. # These activities include: offering paid services to customers as an ASP, # providing data storage and archival services, shipping this software with a # closed source product. # # For more information, please contact Mabulous GmbH at this # address: info@mabulous.com # declare -r TMP_DIR="$(mktemp -d)" mkdir -p "$TMP_DIR"/ltvdir/certs mkdir -p "$TMP_DIR"/ltvdir/crls declare -r TMP_LTV_DIR="$TMP_DIR"/ltvdir #set exit trap to clean up temporary files exit_trap() { local -i EXIT_CODE="$?" rm -rf -- "$TMP_DIR" exit "$EXIT_CODE" } trap "exit_trap" EXIT declare OUT_STREAM=/dev/null #uncomment for verbose output #OUT_STREAM=/dev/stdout #echo red text echo_error() { local RED='\033[0;31m' local NO_COLOR='\033[0m' echo -e "${RED}$1${NO_COLOR}" } #echo yellow text echo_warning() { local YELLOW='\033[1;33m' local NO_COLOR='\033[0m' echo -e "${YELLOW}$1${NO_COLOR}" } #echo light blue text echo_info() { local LIGHT_BLUE='\033[1;34m' local NO_COLOR='\033[0m' echo -e "${LIGHT_BLUE}$1${NO_COLOR}" } #echo light green text echo_success() { local LIGHT_GREEN='\033[1;32m' local NO_COLOR='\033[0m' echo -e "${LIGHT_GREEN}$1${NO_COLOR}" } #echo dark gray text to OUT_STREAM log() { local DARK_GRAY='\033[1;30m' local NO_COLOR='\033[0m' echo -e "${DARK_GRAY}$1${NO_COLOR}" > "$OUT_STREAM" } #assertion #param 1: condition as string #param 2: message assert() { local -r CONDITION="$1" local MESSAGE="$2" if [ -z "$MESSAGE" ]; then MESSAGE="$CONDITION" fi local -r STACK_DEPTH=${#BASH_SOURCE[@]} local -i i local -r BACKTRACE="for ((i=1; i<$STACK_DEPTH; i++)); do echo_error "\"' [$i]: ${BASH_SOURCE[$i]} : ${FUNCNAME[$i]} line ${BASH_LINENO[$i-1]}'\"" done" local -r ASSERTION="if $CONDITION; then : else echo_error "\""Assertion failed: $MESSAGE"\"" echo_error "\""Backtrace:"\"" $BACKTRACE exit 1 fi" if ! eval "$ASSERTION"; then echo_info "$ASSERTION" echo_error "Assertion in ${BASH_SOURCE[0]} on line ${BASH_LINENO[0]} is malformed." exit 1 fi } declare -r TOKEN_HEADER="-----BEGIN RFC3161 TOKEN-----" declare -r TOKEN_FOOTER="-----END RFC3161 TOKEN-----" declare -r SUBJECT_LINE="-----TIMESTAMP COMMIT-----" declare -r -i TIMESTAMPING_VERSION=1 declare -r TRAILER_TOKEN_VERSION="Version:" declare -r TRAILER_TOKEN_PREIMAGE="Preimage:" declare -r TRAILER_TOKEN_ALGO="Algorithm:" declare -r TRAILER_TOKEN_DIGEST="Digest:" declare -r TRAILER_TOKEN_TIMESTAMP="Timestamp:" #get hashing algorithm used by repo declare -r ALGO=$(git rev-parse --show-object-format) #get length of hashes of the used algorithm declare -r -i HASH_LENGTH=$(printf "0" | openssl dgst -"$ALGO" -binary | xxd -p -c 256 | awk '{print length; exit}') #get directory to store validation data declare -r ROOT_DIR=$(git rev-parse --git-dir)/.. declare -r LTV_DIR="$ROOT_DIR"/.timestampltv #get directory for trusted RootCA certificates declare -r CA_PATH=$(git rev-parse --git-path hooks/trustanchors) if [ ! -d "$CA_PATH" ]; then mkdir -p "$CA_PATH" fi # function to assemble the string that is used to create the digest to be timestamped # param1: hash of parent commit # param2: hash of tree # param2: OUT variable, the string used to compute digest get_preimage_from_tree_and_parent() { local -r TREE_HASH="$1" local -r PARENT_COMMIT_HASH="$2" local -n PREIMAGE_OUT="$3" log "get_preimage_from_tree_and_parent for tree $TREE_HASH and commit $PARENT_COMMIT_HASH" #perform precondition checks assert "[ ${#TREE_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $TREE_HASH must have length $HASH_LENGTH." assert "[ ${#PARENT_COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $PARENT_COMMIT_HASH must have length $HASH_LENGTH." PREIMAGE_OUT="version:${TIMESTAMPING_VERSION,,},parent:${PARENT_COMMIT_HASH,,},tree:${TREE_HASH,,}" } # function to compute digest that is timestamped, which is a hash that depends on commit hash and tree hash # param1: hash of parent commit # param2: hash of tree # param2: OUT variable, the DIGEST compute_digest_from_tree_and_parent() { local -r TREE_HASH="$1" local -r PARENT_COMMIT_HASH="$2" local -n DIGEST_OUT="$3" log "compute_digest_from_tree_and_parent for tree $TREE_HASH and commit $PARENT_COMMIT_HASH" #perform precondition checks assert "[ ${#TREE_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $TREE_HASH must have length $HASH_LENGTH." assert "[ ${#PARENT_COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $PARENT_COMMIT_HASH must have length $HASH_LENGTH." local PREIMAGE get_preimage_from_tree_and_parent "$TREE_HASH" "$PARENT_COMMIT_HASH" PREIMAGE DIGEST_OUT=$(printf "%s" "$PREIMAGE" | openssl dgst -"$ALGO" -binary | xxd -p -c 256) #perform postcondition checks assert "[ ${#DIGEST_OUT} -eq $HASH_LENGTH ]" "Postcondition: hash $DIGEST_OUT must have length $HASH_LENGTH." } # function to extract timestamp token from commit. The extracted token are saved to files in the folder provided # and are named *.extracted_token.tst where * is the (0-based) index of the token as ordered within the commit. # param1: hash of commit to extract token from # param2: path to dir into which the token should be saved # param3: OUT variable containing the version of the timestamp commit (0 if no version is defined but timestamp token were found, -1 if this commit does not contain any timestamp token) # param4: OUT array containing the tsa urls from which each timestamp was retrieved # param5: OUT array containing the paths to the extracted timestamp tokens extract_token_from_commit() { local -r COMMIT_HASH="$1" local -r TOKEN_DIR="$2" local -n VERSION_OUT="$3" local -n URL_ARRAY_OUT="$4" local -n TOKEN_ARRAY_OUT="$5" log "extract_token_from_commit $COMMIT_HASH into $TOKEN_DIR" #perform precondition checks assert "[ ${#COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $COMMIT_HASH must have length $HASH_LENGTH." assert "[ -d $TOKEN_DIR ]" "Precondition: directory $TOKEN_DIR must exist." local -r COMMIT_CONTENT=$(git cat-file -p "$COMMIT_HASH") assert "[ ${#COMMIT_CONTENT} -gt 0 ]" "Precondition: $COMMIT_HASH must be a valid commit hash" #remove files from possible previous runs rm -f "$TMP_DIR"/*.extracted_token.pem rm -f "$TMP_DIR"/*.extracted_token.url #extract timestamp tokens local URL_START=${#TRAILER_TOKEN_TIMESTAMP} URL_START=$(( $URL_START + 2)) local -i NUM_EXTRACTED=$(printf "%s" "$COMMIT_CONTENT" | awk '$0~trailerregex{ i++; insidetimestamp=1; print substr($0,urlstart) > tmpdir i ".extracted_token.url" } /-----END/{insidepem=0; insidetimestamp=0} insidepem{print substr($0,2) > tmpdir i ".extracted_token.pem"} insidetimestamp && /-----BEGIN/{insidepem=1} END {print i}' tmpdir="$TMP_DIR/" trailerregex="^$TRAILER_TOKEN_TIMESTAMP" urlstart="$URL_START") if [ -z "$NUM_EXTRACTED" ]; then NUM_EXTRACTED=0 fi local -r TMP_DER="$TMP_DIR"/extracted_token.der local -i IDX=0; local -i i for (( i=1; i<=$NUM_EXTRACTED; i++ )); do local EXTRACTED_PEM_FILE="$TMP_DIR"/"$i".extracted_token.pem local EXTRACTED_TOKEN="$TOKEN_DIR"/"$IDX".extracted_token.tst openssl base64 -d -in "$EXTRACTED_PEM_FILE" -out "$TMP_DER" #since a commit might contain some unrelated trailer with the name "Timestamp:" that also contains PEM header and footer, non-timestamp-tokens should be skipped if ! openssl ts -reply -token_in -token_out -in "$TMP_DER" -out "$EXTRACTED_TOKEN" &> "$OUT_STREAM"; then echo_warning "A PEM encoded trailer labeled $TRAILER_TOKEN has been found in commit $COMMIT_HASH which does not seem to be a timestamp token. Skipping." continue fi local TSA_URL=$(cat "$TMP_DIR"/"$i".extracted_token.url) URL_ARRAY_OUT+=("$TSA_URL") TOKEN_ARRAY_OUT+=("$EXTRACTED_TOKEN") ((IDX++)) done local VERSION_START=${#TRAILER_TOKEN_VERSION} VERSION_START=$(( $VERSION_START + 1)) VERSION_OUT=$(($(printf "%s" "$COMMIT_CONTENT" | awk '$0~trailerregex{print substr($0,versionstart); exit}' trailerregex="^$TRAILER_TOKEN_VERSION" versionstart="$VERSION_START"))) if [ -z "$VERSION_OUT" ]; then VERSION_OUT=0 fi if [ $IDX -eq 0 ]; then VERSION_OUT=-1 fi #perform postcondition checks assert "[ ${#TOKEN_ARRAY_OUT[@]} -eq ${#URL_ARRAY_OUT[@]} ]" "Postcondition: Arrays must have equal length." assert "[ $IDX -gt 0 ] || [ $VERSION_OUT -eq -1 ]" "Postcondition: Version must be -1 if it does not contain token." assert "[ $IDX -eq 0 ] || [ $VERSION_OUT -ge 0 ]" "Postcondition: Version must be 0 or greater if token were extracted." } # function to extract ESSCertID or ESSCertIDv2 of TSA from token # param1: path to token in DER encoding # param2: OUT variable, the ID get_tsa_cert_id() { local TOKEN_FILE="$1" local -n CERT_ID_OUT="$2" log "get_tsa_cert_id for $TOKEN_FILE" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." #this works for both ESSCertID as well as ESSCerrtIDv2 since the version2 identifier is id-smime-aa-signingCertificateV2 CERT_ID_OUT=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" \ | awk '/:id-smime-aa-signingCertificate/{f=1} f && /\[HEX DUMP\]:/ {print; exit}' \ | sed 's/^.*\[HEX DUMP\]://1') #perform postcondition checks assert "[ ! -z $CERT_ID_OUT ]" "Postcondition: Token in file $TOKEN_FILE must contain ESSCertID or ESSCertIDv2" } # function to extract hashing algorithm used in the ESSCertID (always sha1) or ESSCertIDv2 # param1: path to token in DER encoding # param2: OUT variable, the hashing algorithm string get_cert_id_hash_agorithm() { local TOKEN_FILE="$1" local -n ALGO_NAME_OUT="$2" log "get_cert_id_hash_agorithm for $TOKEN_FILE" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." local PARSED=$(openssl asn1parse -inform DER -in "$TOKEN_FILE") if [[ "$PARSED" == *":id-smime-aa-signingCertificateV2"* ]]; then #TODO: extract non-default hashing algorithms ALGO_NAME_OUT="sha256" elif [[ "$PARSED" == *":id-smime-aa-signingCertificate"* ]]; then ALGO_NAME_OUT="sha1" else ALGO_NAME_OUT="unknown" fi #perform postcondition checks assert "[ $ALGO_NAME_OUT != 'unknown' ]" "Postcondition: Token in file $TOKEN_FILE must contain ESSCertID or ESSCertIDv2" } # function to extract digest from token file # param1: path to token in DER encoding # param2: OUT variable, the digest get_token_digest() { local TOKEN_FILE="$1" local -n DIGEST_OUT="$2" log "get_token_digest for $TOKEN_FILE" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." local OFFSET=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" \ | awk '/:id-smime-ct-TSTInfo/{f=1} f && /\[HEX DUMP\]:/ {print; exit}' \ | sed 's/:.*//' | sed 's/^[ \t]*//') DIGEST_OUT=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" -strparse "$OFFSET" \ | awk '/\[HEX DUMP\]:/ {print; exit}' \ | sed 's/^.*\[HEX DUMP\]://1') #perform postcondition checks assert "[ ${#DIGEST_OUT} -eq $HASH_LENGTH ]" "Postcondition: hash $DIGEST_OUT must have length $HASH_LENGTH." } # function to extract unix time of timestamp from token file # param1: path to token in DER encoding # param2: OUT variable, the unix time get_token_unix_time() { local TOKEN_FILE="$1" local -n UNIXTIME="$2" log "get_token_unix_time for $TOKEN_FILE" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." local TOKEN_TIMESTAMP=$(openssl ts -reply -in "$TOKEN_FILE" -token_in -token_out -text 2> "$OUT_STREAM" \ | awk '/Time stamp:/{f=1} f {print; exit}' \ | sed 's/^.*Time stamp: //1') UNIXTIME=$(date "+%s" -d "$TOKEN_TIMESTAMP") #perform postcondition checks assert "[ ! -z $UNIXTIME ]" "Postcondition: Token in file $TOKEN_FILE must contain valid timestamp" } #function to request a timestamp for a specified digest # param1: tsa url # param2: digest # param3: whether to request certificates to be included (true or false) # param4: the file to output the token to request_token() { local TSA_URL="$1" local DIGEST="$2" local REQUEST_CERTS="$3" local OUTPUT_FILE="$4" log "request_token for digest $DIGEST from url $TSA_URL. REQUEST_CERTS=$REQUEST_CERTS" #perform precondition checks assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH." local CONTENT_TYPE="Content-Type: application/timestamp-query" local ACCEPT_TYPE="Accept: application/timestamp-reply" local REQ_FILE="$TMP_DIR"/token_req.tsq if [ "$REQUEST_CERTS" = true ]; then if ! openssl ts -query -cert -digest "$DIGEST" -"$ALGO" -out "$REQ_FILE" &> "$OUT_STREAM"; then echo "Error: Failed to create token query" return 1 fi else if ! openssl ts -query -digest "$DIGEST" -"$ALGO" -out "$REQ_FILE" &> "$OUT_STREAM"; then echo "Error: Failed to create token query" return 1 fi fi local RESPONSE_FILE="$TMP_DIR"/response.tsr if ! curl "$TSA_URL" -H "$CONTENT_TYPE" -H "$ACCEPT_TYPE" --data-binary @"$REQ_FILE" --output "$RESPONSE_FILE" &> "$OUT_STREAM"; then echo "Error: Failed to get response from $TSA_URL" return 1 fi local RESPONSE_STATUS=$(openssl ts -reply -in "$RESPONSE_FILE" -text 2> "$OUT_STREAM" | awk '/Status: /{print; exit}' | sed 's/Status: //' | sed 's/\.//') if [ "$RESPONSE_STATUS" != "Granted" ]; then echo "Error: Token request was not granted." if [ -z "$RESPONSE_STATUS" ]; then cat "$RESPONSE_FILE" echo "" else local STATUS_INFO=$(openssl ts -reply -in "$RESPONSE_FILE" -text 2> "$OUT_STREAM" | awk '/Status info:/{f=1} f {print} /Failure info: /{exit}') echo "$STATUS_INFO" echo "Note: If rejection reason is unrecognized or unsupported algorithm, then this tsa cannot be used for this repository, since it uses --object-format=$ALGO" echo "The token request was:" openssl ts -query -in "$REQ_FILE" -text 2> "$OUT_STREAM" fi return 1 fi if ! openssl ts -reply -in "$RESPONSE_FILE" -token_out -out "$OUTPUT_FILE" &> "$OUT_STREAM"; then echo "Error: Not a valid TSA response in file $RESPONSE_FILE" return 1 fi } #builds a certificate chain for token. The passed token must have been requested with -cert option # and with matching digest. # param1: token file. Token must have been requested with -cert option and with digest of param2 # param2: the digest the token was requested for # param3: the tsa url from which the token was requested # param4: the output file for the chain. It contains all certificates in order, with the first # one being the TSA cetificate and the last one the self-signed root certificate. build_certificate_chain_for_token() { local TOKEN_FILE="$1" local DIGEST="$2" local TSA_URL="$3" local OUT_CERT_FILE="$4" log "build_certificate_chain_for_token for token $TOKEN_FILE from $TSA_URL with digest $DIGEST and store in certificate file $OUT_CERT_FILE" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH." local DUMMY_TOKEN="$TMP_DIR"/dummy_token.tst local ALL_EXTRACTED_CERTS="$TMP_DIR"/extracted_certs.pem local CHAIN=() #if the TSA uses multiple certificates to sign tokens it may take a few attempts to get one containint the proper signer #TODO: allow to set maximum retry attempts local SIGNING_CERT_ID="" get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID local CERT_ID_HASH_ALGO="" get_cert_id_hash_agorithm "$TOKEN_FILE" CERT_ID_HASH_ALGO local -i i for i in {1..10} ;do #request dummy token. Use current commit digest request_token "$TSA_URL" "$DIGEST" true "$DUMMY_TOKEN" #extract certifcates openssl pkcs7 -inform DER -in "$DUMMY_TOKEN" -print_certs -outform PEM -out "$ALL_EXTRACTED_CERTS" &> "$OUT_STREAM" #remove files from previous runs rm -f "$TMP_DIR"/*.extracted.pem.cer rm -f "$TMP_DIR"/cert_chain_*.pem.cer #extract all individual certificates from ALL_EXTRACTED_CERTS cat "$ALL_EXTRACTED_CERTS" \ | awk '/-----BEGIN CERTIFICATE-----/ { i++; } /-----BEGIN CERTIFICATE-----/, /-----END CERTIFICATE-----/ \ { print > tmpdir i ".extracted.pem.cer" }' tmpdir="$TMP_DIR/" #find cetificate that signed token local EXTRACTED_CERT while read EXTRACTED_CERT; do local CERT_ID=$(openssl x509 -inform PEM -in "$EXTRACTED_CERT" -outform DER | openssl dgst -"$CERT_ID_HASH_ALGO" -binary | xxd -p -c 256) #if openssl ts -verify -digest "$DIGEST" -in "$TOKEN_FILE" -token_in -partial_chain -CAfile "$EXTRACTED_CERT" &> "$OUT_STREAM"; then if [ "${SIGNING_CERT_ID,,}" == "${CERT_ID,,}" ]; then #found the signer certificate CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer) mv -f "$EXTRACTED_CERT" "${CHAIN[-1]}" break 2 fi done <<< $(ls "$TMP_DIR"/*.extracted.pem.cer) done if [ ${#CHAIN[@]} -eq 0 ]; then echo "Unable to download token that contains signing cert for this token:" openssl ts -reply -token_in -token_out -in "$TOKEN_FILE" -text return 1 fi #iterate until self-signed certificate is reached while ! openssl verify -CAfile "${CHAIN[-1]}" "${CHAIN[-1]}" &> "$OUT_STREAM"; do #try to find parent certificate in extracted certs if ls "$TMP_DIR"/*.extracted.pem.cer &> "$OUT_STREAM"; then while read EXTRACTED_CERT; do if openssl verify -partial_chain -CAfile "$EXTRACTED_CERT" "${CHAIN[-1]}" &> "$OUT_STREAM"; then CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer) mv -f "$EXTRACTED_CERT" "${CHAIN[-1]}" continue 2 fi done <<< $(ls "$TMP_DIR"/*.extracted.pem.cer) fi #otherwise try to find in trust store if ls "$CA_PATH"/*.0 &> "$OUT_STREAM"; then local TRUSTED_CERT while read TRUSTED_CERT; do if openssl verify -partial_chain -CAfile "$TRUSTED_CERT" "${CHAIN[-1]}" &> "$OUT_STREAM"; then CHAIN+=("$TRUSTED_CERT") continue 2 fi done <<< $(ls "$CA_PATH"/*.0) fi #otherwise try to download local URL=$(openssl x509 -inform PEM -noout -text -in "${CHAIN[-1]}" \ | awk '/Authority Information Access:/{f=1} f && /CA Issuers - URI:/ {print; exit}' \ | sed 's/^.*CA Issuers - URI://1') if [ -z "$URL" ]; then echo "Certificate ${CHAIN[-1]} does not contain Authority Information Access extension with CA issuer URL. Can't build certificate chain." return 1 fi CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer) local TMP_DOWNLOAD="$TMP_DIR"/tmp_download.crt if ! curl "$URL" --output "$TMP_DOWNLOAD" &> "$OUT_STREAM"; then echo "Failed to download issuer certificate from $URL" return 1 fi #convert from DER to PEM if necessary if openssl x509 -inform PEM -in "$TMP_DOWNLOAD" -noout &> "$OUT_STREAM"; then openssl x509 -inform PEM -in "$TMP_DOWNLOAD" -outform PEM -out "${CHAIN[-1]}" elif openssl x509 -inform DER -in "$TMP_DOWNLOAD" -noout &> "$OUT_STREAM"; then openssl x509 -inform DER -in "$TMP_DOWNLOAD" -outform PEM -out "${CHAIN[-1]}" else echo "Unknown certificate file format for $URL" return 1 fi done echo -n > "$OUT_CERT_FILE" local CERT for CERT in "${CHAIN[@]}"; do openssl x509 -in "$CERT" -noout -subject >> "$OUT_CERT_FILE" echo '' >> "$OUT_CERT_FILE" openssl x509 -in "$CERT" -noout -issuer >> "$OUT_CERT_FILE" echo '' >> "$OUT_CERT_FILE" cat "$CERT" >> "$OUT_CERT_FILE" echo '' >> "$OUT_CERT_FILE" done } # Tries to download CRLs for the entire chain and store them together in PEM encoding in an output file. # param1: path to the certificate chain in PEM format # param2: path to output file # TODO: performance of this could be improved by using OCSPs to check for changes first download_crls_for_chain() { local CERT_FILE="$1" local OUTPUT_FILE="$2" log "download_crls_for_chain for certificate file $CERT_FILE and store to $OUTPUT_FILE" #perform precondition checks assert "[ ! -z $CERT_FILE ]" "Precondition: Path to certificate file must not be empty." assert "[ -f $CERT_FILE ]" "Precondition: Certificate file $CERT_FILE must exist." echo -n > "$OUTPUT_FILE" local CRL_TMP="$TMP_DIR"/crl_tmp.crl #remove files from possible previous runs rm -f "$TMP_DIR"/*.extracted.pem.cer #extract all contained certificates into separate files cat "$CERT_FILE" \ | awk '/-----BEGIN CERTIFICATE-----/ { i++; } /-----BEGIN CERTIFICATE-----/, /-----END CERTIFICATE-----/ \ { print > tmpdir i ".extracted.pem.cer" }' tmpdir="$TMP_DIR/" local NUM_EXTRACTED=$(find "$TMP_DIR" -maxdepth 1 -name "*.extracted.pem.cer" -printf '.' | wc -m) assert "[ $NUM_EXTRACTED -gt 0 ]" "Precondition: Certificate file $CERT_FILE must contain at least one certificate in PEM format." #iterate over certificates. Ignore self-signed certificates local EXTRACTED_CERT ls "$TMP_DIR"/*.extracted.pem.cer | while read EXTRACTED_CERT; do if ! openssl verify -CAfile "$EXTRACTED_CERT" "$EXTRACTED_CERT" &> "$OUT_STREAM"; then local URL=$(openssl x509 -inform PEM -in $EXTRACTED_CERT -text -noout \ | awk '/CRL Distribution Points:/{f=1} f && /URI:/ {print; exit}' \ | sed 's/^.*URI://1') if curl -L "$URL" --output "$CRL_TMP" &> "$OUT_STREAM"; then if openssl crl -in "$CRL_TMP" -inform DER -noout &> "$OUT_STREAM"; then openssl crl -in "$CRL_TMP" -inform DER >> "$OUTPUT_FILE" elif openssl crl -in "$CRL_TMP" -inform PEM -noout &> "$OUT_STREAM"; then openssl crl -in "$CRL_TMP" -inform PEM >> "$OUTPUT_FILE" else echo "Unknown CRL file format for $URL" return 1 fi else echo "Failed to download CRL from $URL" return 1 fi fi done } # Check whether the file containing the certificates to verify the token are available and if not, # request and add them to the commit. # param1: path to token in DER encoding # param2: digest to verify # param3: tsa url verify_token_and_add_ltv_data() { local TOKEN_FILE="$1" local DIGEST="$2" local TSA_URL="$3" log "verify_token_and_add_ltv_data for token $TOKEN_FILE and digest $DIGEST from url $TSA_URL" #perform precondition checks assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty." assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist." assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH." local SIGNING_CERT_ID get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer if [ ! -s "$CERT_CHAIN_FILE" ]; then CERT_CHAIN_FILE="$TMP_LTV_DIR"/certs/"$SIGNING_CERT_ID".cer #try to build full chain. if ! build_certificate_chain_for_token "$TOKEN_FILE" "$DIGEST" "$TSA_URL" "$CERT_CHAIN_FILE"; then echo "Unable to build certificate chain." return 1 fi if ! openssl verify --CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" &> "$OUT_STREAM"; then echo "TSA certificate from $TSA_URL is not trusted. Check your trustanchors in $CA_PATH" return 1 fi fi #verify token and download CRL data local CRL_CHAIN_FILE="$TMP_LTV_DIR"/crls/"$SIGNING_CERT_ID.crl" #only download CRL data if it hasn't been already in a previous step if [ ! -s "$CRL_CHAIN_FILE" ]; then if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$CRL_CHAIN_FILE"; then echo "Could not download CRL data for $TOKEN_FILE" return 1 fi fi #verify signing certificate local TOKEN_UNIXTIME='' get_token_unix_time "$TOKEN_FILE" TOKEN_UNIXTIME #validate signing certificate if ! openssl verify -attime "$TOKEN_UNIXTIME" -CApath "$CA_PATH" -CRLfile "$CRL_CHAIN_FILE" \ -crl_check_all -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" &> "$OUT_STREAM"; then echo "TSA certificate from $TSA_URL could not be validated." return 1 fi #validate token if ! openssl ts -verify -digest "$DIGEST" -in "$TOKEN_FILE" -token_in -attime "$TOKEN_UNIXTIME" \ -CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" 2> "$OUT_STREAM"; then echo "Token from $TSA_URL could not be validated." return 1 fi }