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
This commit is contained in:
Matthias Bühlmann
2021-02-21 18:19:05 +01:00
parent 3556989de0
commit 80034aeb78
8 changed files with 553 additions and 602 deletions

View File

@@ -37,20 +37,20 @@
# address: info@mabulous.com
#
TMP_DIR="$(mktemp -d)"
declare -r TMP_DIR="$(mktemp -d)"
mkdir -p "$TMP_DIR"/ltvdir/certs
mkdir -p "$TMP_DIR"/ltvdir/crls
TMP_LTV_DIR="$TMP_DIR"/ltvdir
declare -r TMP_LTV_DIR="$TMP_DIR"/ltvdir
#set exit trap to clean up temporary files
exit_trap() {
local EXIT_CODE="$?"
local -i EXIT_CODE="$?"
rm -rf -- "$TMP_DIR"
exit "$EXIT_CODE"
}
trap "exit_trap" EXIT
OUT_STREAM=/dev/null
declare OUT_STREAM=/dev/null
#uncomment for verbose output
#OUT_STREAM=/dev/stdout
@@ -82,21 +82,162 @@ log() {
echo -e "${DARK_GRAY}$1${NO_COLOR}" > "$OUT_STREAM"
}
TOKEN_HEADER="-----BEGIN RFC3161 TOKEN-----"
TOKEN_FOOTER="-----END RFC3161 TOKEN-----"
SUBJECT_LINE="-----TIMESTAMP COMMIT-----"
TRAILER_TOKEN="Timestamp:"
#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
ALGO=$(git rev-parse --show-object-format)
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
ROOT_DIR=$(git rev-parse --git-dir)/..
LTV_DIR="$ROOT_DIR"/.timestampltv
declare -r ROOT_DIR=$(git rev-parse --git-dir)/..
declare -r LTV_DIR="$ROOT_DIR"/.timestampltv
#get directory for trusted RootCA certificates
CA_PATH=$(git rev-parse --git-path hooks/trustanchors)
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
@@ -104,16 +245,18 @@ 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')
if [ -z "$CERT_ID_OUT" ]; then
echo "Token $TOKEN_FILE does not contain ESSCertID or ESSCertIDv2 of issuer."
return 1
fi
return 0
#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
@@ -123,9 +266,13 @@ 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"
@@ -134,7 +281,9 @@ get_cert_id_hash_agorithm() {
else
ALGO_NAME_OUT="unknown"
fi
return 0
#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
@@ -145,13 +294,19 @@ get_token_digest() {
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')
return 0
#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
@@ -162,11 +317,17 @@ get_token_unix_time() {
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")
return 0
#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
@@ -181,6 +342,9 @@ request_token() {
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"
@@ -220,21 +384,26 @@ request_token() {
echo "Error: Not a valid TSA response in file $RESPONSE_FILE"
return 1
fi
return 0
}
#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 output file for the chain. It contains all certificates in order, with the first
# 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 CERT_FILE="$4"
log "build_certificate_chain_for_token for token $TOKEN_FILE with digest $DIGEST and certificate file $CERT_FILE"
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
@@ -328,16 +497,15 @@ build_certificate_chain_for_token() {
fi
done
echo -n > "$CERT_FILE"
echo -n > "$OUT_CERT_FILE"
for CERT in "${CHAIN[@]}"; do
openssl x509 -in "$CERT" -noout -subject >> "$CERT_FILE"
echo '' >> "$CERT_FILE"
openssl x509 -in "$CERT" -noout -issuer >> "$CERT_FILE"
echo '' >> "$CERT_FILE"
cat "$CERT" >> "$CERT_FILE"
echo '' >> "$CERT_FILE"
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
return 0
}
# Tries to download CRLs for the entire chain and store them together in PEM encoding in an output file.
@@ -349,6 +517,10 @@ download_crls_for_chain() {
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
@@ -379,7 +551,6 @@ download_crls_for_chain() {
fi
fi
done
return 0
}
# Check whether the file containing the certificates to verify the token are available and if not,
@@ -393,7 +564,12 @@ verify_token_and_add_ltv_data() {
local TSA_URL="$3"
log "verify_token_and_add_ltv_data for token $TOKEN_FILE and digest $DIGEST from url $TSA_URL"
local SIGNING_CERT_ID=''
#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
@@ -410,7 +586,13 @@ verify_token_and_add_ltv_data() {
fi
#verify token and download CRL data
local CRL_CHAIN_FILE="$TMP_LTV_DIR"/crls/"$SIGNING_CERT_ID.crl"
download_crls_for_chain "$CERT_CHAIN_FILE" "$CRL_CHAIN_FILE"
#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
@@ -426,5 +608,4 @@ verify_token_and_add_ltv_data() {
echo "Token from $TSA_URL could not be validated."
return 1
fi
return 0
}