Files
GitTrustedTimestamps/hooks/timestamping
Matthias Bühlmann 80034aeb78 Changed digest that is being timestamped
Changed digest that is being stamped from $parent_commit_hash, to
shaX(parent:$parent_commit_hash,tree:$tree_hash)
where shaX is the hash function used by the repository.
This change is so that the timestamp added also timestamps the
LTV data that is being added with the timestamp commit.

This LTV data now also contains CRLs for the LAST timestamp commit.
This ensures that timestamp lifetime of old timestamps gets
arbitrarily extended into the future with every new timestamp
added to the repository.

Further changes:
-Updated documentation
-updated schematics and changed from SVG to PNG
-added assertions, pre- and post-conditions
-added version number to timestamp commits as trailer
-added hashing algorithm used as trailer
-added digest being timestamped as trailer
-added the string that is hashed to get the digest as traile
-improved log messages of validate.sh
2021-02-21 18:19:05 +01:00

611 lines
26 KiB
Bash

#!/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 <https://www.gnu.org/licenses/>.
#
# 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 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 -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;
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
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
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
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"
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/"
#iterate over certificates. Ignore self-signed certificates
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 "$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 [ ! -f "$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 [ ! -f "$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
}