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:
42
README.md
42
README.md
@@ -10,7 +10,7 @@ By using this post-commit hook in a repository and thereby adding secure timesta
|
||||
|
||||
# How to use this software
|
||||
|
||||
0. (optional, but recommended) If you're ceating a new repository, it is strongly recommended to use SHA256 hashes (git uses SHA1 by default at the time of writing) by initializing the reopository using `git init --object-format=sha256` (Note: If you want to use a public hosting server such as github for your repository, you should check whether they already support SHA256 repositories). For more information, see https://git-scm.com/docs/hash-function-transition/
|
||||
0. (optional, but recommended) If you're ceating a new repository, it is strongly recommended to use SHA256 hashes (git uses SHA1 by default at the time of writing) by initializing the repository using `git init --object-format=sha256` (Note: If you want to use a public hosting server such as github for your repository, you should check whether they already support SHA256 repositories). For more information, see https://git-scm.com/docs/hash-function-transition/
|
||||
1. Copy the four bash scripts in the [hooks](hooks/) folder of this project into the .git/hooks folder of the project you want to timestamp.
|
||||
2. Configure the TSA url you want to use (in this example https://freetsa.org/tsr) using
|
||||
`git config --local timestamping.tsa0.url https://freetsa.org/tsr`
|
||||
@@ -20,8 +20,9 @@ By using this post-commit hook in a repository and thereby adding secure timesta
|
||||
5. (optional) By default, a commit will fail if a timestamp token cannot be retrieved. If you want to make timestamping optional for a certain tsa, you can set
|
||||
`git config --local --type=bool timestamping.tsa0.optional true`.
|
||||
If `optional` is set to true and a timestamping token cannot be retrieved, you will receive a warning but the commit will be created nevertheless.
|
||||
7. (optional) You might want to add this README.md and the [docs](docs/) folder or this repository to your repository as well, so that documentation of the timestamps is guaranteed to be available if the timestamps should be evaluated many years in the future.
|
||||
|
||||
From now on, every `git commit` will tigger an additional commit that securely timestamps this commit.
|
||||
From now on, every `git commit` will automatically tigger an additional commit that securely timestamps it.
|
||||
|
||||
# Implementation design
|
||||
|
||||
@@ -37,15 +38,15 @@ By embedding the timestamps in the commit history, they form a hash-chain and th
|
||||
|
||||
The design leverages git's Merkle-Tree layout and embeds the timestmaps in the commit history, making them form a hash-chain that prevents later changes without being noticed:
|
||||
|
||||

|
||||

|
||||
|
||||
Or as a further simplified schematic:
|
||||
|
||||
<img src="./docs/schematic_simplified.svg" alt="Simplified Merkle-Tree" width="250">
|
||||
<img src="./docs/schematic_simplified.png" alt="Simplified Merkle-Tree" width="500">
|
||||
|
||||
# What are RFC3161 and RFC5816 Timestamps
|
||||
|
||||
RFC3161 (https://tools.ietf.org/html/rfc3161) and its extension RFC5816 (https://tools.ietf.org/html/rfc5816) are protocol specifications timestamp data using cryptographically secure tokens issued by an external, trusted third party TSA (Time Stamping Authority). By timestamping data this way, it is possible to prove to anyone who trusts this TSA service that the data existed already at the time of timestamping and has not been tampered with ever since. Only a secure hash of the data, without any identification, is being sent to the TSA service, so the data itself remains secret.
|
||||
RFC3161 (https://tools.ietf.org/html/rfc3161) and its extension RFC5816 (https://tools.ietf.org/html/rfc5816) are protocol specifications to timestamp data using cryptographically secure tokens issued by an external, trusted third party TSA (Time Stamping Authority). By timestamping data this way, it is possible to prove to anyone who trusts this TSA service that the data existed already at the time of timestamping and has not been tampered with ever since. Only a secure hash of the data, without any identification, is being sent to the TSA service, so the data itself remains secret.
|
||||
|
||||
# Alternatives
|
||||
|
||||
@@ -68,11 +69,11 @@ Zeitgitter seems to use a custom timestamping protocol and rely on developers cr
|
||||
|
||||
# How are timestamps added to commits
|
||||
|
||||
For each commit that is being timestamped, an additional *timestamp commit* is created, for which the commit that is being timestamped is the direct parent. The hash that is contained in the timestamp token corresponds to the git hash of the commit being timestamped. Since git itself is implemented as a [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree), this hash depends on every bit of every commited file as well as the entire commit history, making it impossible to change anything without invalidating the timestamp. The timestamping token (one for each TSA for which a timestamp was retrieved) is then added in PEM encoding (plus some info about the token in readable form) as a trailer to the commit message of the *timestamp commit*. Chosing this design to add the timestamps has several advantages:
|
||||
For each commit that is being timestamped, an additional *timestamp commit* is created, for which the commit that is being timestamped is the direct parent. The digest hash that is contained in the timestamp token is a hash derived from the commit hash of that parent commit as well as the tree hash of this *timestamp commit*. Since git itself is implemented as a [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree), this hash hence depends on every bit of every commited file as well as the entire commit history, making it impossible to change anything without invalidating the timestamp. The timestamping token (one for each TSA for which a timestamp was retrieved from) is then added in PEM encoding (plus some info about the token in readable form) as a trailer to the commit message of the *timestamp commit*. Chosing this design to add the timestamps has several advantages:
|
||||
- The commit hash always depends on the entire data of that commit (including the pgp signature that commit is signed with) and its history, meaning that not a single bit of data being committed (or the history it depends on) could be changed without creating a completely different commit hash.
|
||||
- It is most likely the most forward-compatible option. If new commit headers or other commit data will be added to git in the future, they will most likely also be captured by the commit hash.
|
||||
- By storing the tokens inside the commit message, which is hashed itself, rather than in git-notes or tags, subsequent commits will *seal* these timestamps, making it impossible to "lose" them, which gives the timestamping of the repository a *non-repudiation* property (https://en.wikipedia.org/wiki/Non-repudiation), meaning it will be impossible to "rewrite history" unnoticed.
|
||||
- Since newer timestamps *seal* older timestamps (i.e. the older timestamps are part of what is being timestamped), this protects old timestamps from becoming invalid in some situations. For example: Say in 2021 all commits are timestamped using a TSA that uses "certificateA" to sign its timestamps. Then in 2022 a TSA is used which uses "certificateB" to sign its timestamps. Then in 2023 the private key of "certificateA" leaks. Normally, if a TSA's private key leaks, all old timestamps become invalid and can't be trusted anymore (because the private key could be used to "backdate" data) - however, since in this case the timestamps of 2021, which are signed with the still trusted "certificateB" *sealed* the older timestamps, these old timestamps can still be considered valid, since they were provably created before the "certificateA" private key leaked. The same applies to old timestamps becoming invalid for example due to the algorithm used for them not being deemed secure anymore.
|
||||
- Since newer timestamps *seal* older timestamps (i.e. the older timestamps are part of what is being timestamped), this protects old timestamps from becoming invalid in some situations and can extend their validatability arbitrarily into the future (see Long Term Validation below). For example: Say in 2021 all commits are timestamped using a TSA that uses "certificateA" to sign its timestamps. Then in 2022 a TSA is used which uses "certificateB" to sign its timestamps. Then in 2023 the private key of "certificateA" leaks. Normally, if a TSA's private key leaks, all old timestamps become invalid and can't be trusted anymore (because the private key could be used to "backdate" data) - however, since in this case the timestamps of 2021, which are signed with the still trusted "certificateB" *sealed* the older timestamps, these old timestamps can still be considered valid, since they were provably created before the "certificateA" private key leaked. The same applies to old timestamps becoming invalid for example due to the algorithm used for them not being deemed secure anymore or if a CA stops issuing CRL data for the TSA certificate.
|
||||
|
||||
An altenative design that was considered but dismissed was to include the timestamps right into the commit message of the commit that is being timestamped (in order to keep the commit history more tidy), in a similar fashion as PGP signatures are added. PGP signatures do this by calculating the commit hash AS-IF the signature was not contained, then sign this hash and then add the signature into the commit header (thereby changing the hash). A similar approach could have been taken with the timestamps, but this would have two serious drawbacks:
|
||||
1. Since PGP signatues are inserted natively AFTER the commit is generated, the timestamp token could therefore not timestamp the signature (instead, the signature would sign the timestamp, which is not useful).
|
||||
@@ -88,15 +89,28 @@ Additionally to retrieving TSA tokens and timestamping the commits with them, th
|
||||
If a token does not pass these tests, it is not added and the commit is either aborted (if the TSA is not set to optional) or a warning is output (if the TSA is set to optional).
|
||||
|
||||
This repository uses the post-commit hook itself, so if you check the commit history of this repository, you will see that each commit is followed by a -----TIMESTAMP COMMIT----- that contains one or more timestamp tokens.
|
||||
For example, [this](https://github.com/MrMabulous/GitTrustedTimestamps/commit/a3e7a2a4a280fc03abe51ff70a8bd603837af150) *timestamp commit* timestamps [this](https://github.com/MrMabulous/GitTrustedTimestamps/commit/4f88284a578ba8309269a4d1f2474033fe441e82) regular commit, which is its direct parent. You can see that the "Message data" of the timestamp token is the commit hash 4f88284a578ba8309269a4d1f2474033fe441e82 of the commit being timestamped.
|
||||
For example, [this](https://github.com/MrMabulous/GitTrustedTimestamps/commit/67a93e8b5dc58f828e9f364ace03ac48a007c778) *timestamp commit* timestamps [this](https://github.com/MrMabulous/GitTrustedTimestamps/commit/6e9841d157c222e9bd79d6580dd029adccf55cde) regular commit, which is its direct parent. You can see that the "Digest" that is timestamped by the token is `bf8d5c510b87e22c06f3300e5dbe69f5c6a3f1e1=sha1(parent:6e9841d157c222e9bd79d6580dd029adccf55cde,tree:75be5fcd56edaff4da1fc31d3632da14618f7fc0)`, whereas the tree hash and parent hash in the preimage correspond to the parent and tree of this *timestamp commit*.
|
||||
Since github did not support sha256 hashes yet at the time this repository was created, the hashing algorithm used is sha1 (for a repository initialized with `git init --object-format=sha256` the hashing algorithm will be sha256 or other, once git adds support for further algorithms)
|
||||
|
||||
# LTV data:
|
||||
# For how long are timestamps valid?
|
||||
|
||||
Additionally to the bare timestamp tokens stored in the commit message as trailers, the *timestamp commit* also adds revisioned files to the .timestampltv folder. If the timestamps should be evaluated many years in the future, when the TSA does not exist anymore and the entire certificate chains of tokens for example can't be retrieved anymore, this *Long Term Validation* data will facilitate validating the tokens. For each *timestamp commit* two files are stored:
|
||||
Generally, a RFC3161 timestamp can be trusted for as long as
|
||||
- the private key of the signing certificate or a certificate in its trustchain did not get compromised and
|
||||
- the hashing algorithm used by the timestamp and the signature algorithm used by the signing certificate is still considered secure
|
||||
|
||||
To be sure of the first point, one must know the current revocation status of the signing certificate. This in turn means that once new CRLs for the certificate that signed the timestamp are not issued anymore, the timestamp shouldn not be trusted anymore, since timestamps could be forged if the private key of the signing certificate leaked.
|
||||
However, if one has a copy of a historic CRL that shows the signature certificate to be valid, and which is timestamped by a timestamp that still can be trusted, then also the compromised timestamp can still be trusted.
|
||||
GitTrustedTimestamp enables this extension of the lifetime of timestamps by storing *Long Term Validation* data for previous timestamps with each new timestamp added.
|
||||
|
||||
# LTV - Long Term Validation:
|
||||
|
||||
Additionally to the bare timestamp tokens stored in the commit message as trailers, the *timestamp commit* also adds revisioned files to the .timestampltv folder. If the timestamps should be evaluated many years in the future, when the TSA does not exist anymore and the entire certificate chains of tokens for example can't be retrieved anymore, this *Long Term Validation* data will help to prove the validity of the tokens. For each timestamp in a *timestamp commit* two types files are stored:
|
||||
1. .timestampltv/certs/issuer_hash.cer: This file contains the entire trust chain of the TSA certificate in PEM format (the first certificate being the TSA's signing certificate and the last being the self-signed root). In most cases this file will not change for subsequent timestamp tokens, so no additional data is added to the repository (the file content only changes when the TSA changes its signing certificate).
|
||||
2. .timestampltv/crls/issuer_hash.crl: This file contains CRL responses in PEM format for all certificates in the trust chain at the time of timestamping. In most cases this file will not change for subsequent timestamp tokens, so no additional data is added to the repository (the file content only changes when the CRL lists referenced by certificate in the trust chain change).
|
||||
2. .timestampltv/crls/issuer_hash.crl: This file contains CRL responses in PEM format for all certificates in the trust chains of the new timestamp tokens at the time of timestamping. In most cases, this file will not change for subsequent timestamp tokens (or only a few lines plus the signature changes), so no or very little additional data is added to the repository (the file content only changes when the CRL lists referenced by certificates in the trust chain change).
|
||||
|
||||
The `issuer_hash` for both files corresponds to the ESSCertID or ESSCertIDv2 hash with which the token identifies its issuer certificate. In general this is the SHA1 hash of the DER encoded issuer certificate for RFC3161 tokens, and some other hash of the DER encoded issuer certificate for RFC5816 tokens (the ESSCertIDv2 of the token specifies the used hashing algorithm).
|
||||
Additionally to the LTV data of the newly added tokens, also the current CRL data of timestamps in the LAST *timestamp commit* will be added (in most cases, these will be the same as the CRLs for the new timestamps if still the same TSAs are used). This allows to extend the validatability of timestamps arbitrarily into the future, so long as new *timestamp commits* are added to the repository while the CA still provides CRL data for the most recently added timestamps (which they are often required to do for many years).
|
||||
|
||||
The `issuer_hash` for these files corresponds to the ESSCertID or ESSCertIDv2 hash using which the token identifies its issuer certificate. In general this is the SHA1 hash of the DER encoded issuer certificate for RFC3161 tokens, and some other hash of the DER encoded issuer certificate for RFC5816 tokens (the ESSCertIDv2 of the token specifies the used hashing algorithm).
|
||||
|
||||
# How to validate timestamps:
|
||||
|
||||
@@ -104,14 +118,14 @@ Ultimately the responsibility (and criteria) for validating the timestamps lies
|
||||
This repository does come however with an [implementation](hooks/validate.sh) that validates the created timestamps according to the criteria listed below. To use it, simply run `.git/hooks/validate.sh`
|
||||
|
||||
`validate.sh` will iterate over the entire commit history of the current branch and for each *timestamp commit* will:
|
||||
- Check that the digest contained in the token matches the commit hash of the timestamped commit
|
||||
- Check that the digest contained in the token matches the hash calculated based on parent hash and tree hash
|
||||
- Checks that the TSA certificate was valid at the time of timestamping, by using historic CRL data
|
||||
- Checks whether the TSA certificate or any intermediate certificate in the chain has been revoked and if so, whether the revocationCode matches the acceptable revocation reasons discussed in chapter 4 of the RFC3161 specification (https://tools.ietf.org/html/rfc3161)
|
||||
- A commit containing multiple timestamps will be considered valid if there is at least one valid timestamp token (a warning will be printed if there are additional timestamps that are considered invalid)
|
||||
|
||||
To perform these checks, the same trusted rootCAs from the `.git/hooks/trustanchors` folder are used.
|
||||
No other checks are performed. In particular, a timestamp-token is considered valid beyond the expiration date of its signing certificate if it hasn't been revoked due to a reason other than those specified in chapter 4.1 of the RFC3161 specification.
|
||||
The curent implementation of the validate.sh script also does not consider whether the hashing algorithms used in the timestamp-token or the keylength of the signing certificate are still considered secure.
|
||||
The curent implementation of the validate.sh script also does not consider whether the hashing algorithms used in the timestamp-token or the keylength of the signing certificate are still considered secure, as this depends on what the validating party considers secure.
|
||||
|
||||
The script exits with exit code 0 if all tests passed, and with exit code 1 otherwise.
|
||||
|
||||
|
||||
BIN
docs/schematic.png
Normal file
BIN
docs/schematic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 171 KiB |
BIN
docs/schematic_simplified.png
Normal file
BIN
docs/schematic_simplified.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 34 KiB |
@@ -40,6 +40,7 @@
|
||||
TSA0_URL=$(git config timestamping.tsa0.url)
|
||||
if [ -z "$TSA0_URL" ]; then
|
||||
# Do nothing if TSA0 has not been configured.
|
||||
echo_info "Info: No timestamping TSA has been configured."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -49,7 +50,9 @@ if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
|
||||
|
||||
extended_exit_trap() {
|
||||
local EXIT_CODE="$?"
|
||||
if [ "$EXIT_CODE" -gt 0 ]; then
|
||||
log "extended_exit_trap $EXIT_CODE"
|
||||
|
||||
if [ $EXIT_CODE -gt 0 ]; then
|
||||
echo_error "Aborting commit."
|
||||
git reset --soft HEAD^
|
||||
fi
|
||||
@@ -58,6 +61,13 @@ extended_exit_trap() {
|
||||
}
|
||||
trap "extended_exit_trap" EXIT
|
||||
|
||||
if [ ! -d "$LTV_DIR"/certs ]; then
|
||||
mkdir -p "$LTV_DIR"/certs
|
||||
fi
|
||||
if [ ! -d "$LTV_DIR"/crls ]; then
|
||||
mkdir -p "$LTV_DIR"/crls
|
||||
fi
|
||||
|
||||
COMMIT_MSG=$(git show --pretty=format:"%B" --no-patch HEAD)
|
||||
|
||||
#avoid recursion and validate timestamp
|
||||
@@ -66,70 +76,160 @@ if [[ "$COMMIT_MSG" == "$SUBJECT_LINE"* ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo_info "Adding Timestamp commit"
|
||||
echo_info "Adding Timestamp commit. This may take a moment..."
|
||||
|
||||
#prepare commit message
|
||||
COMMIT_MSG_FILE="$TMP_DIR"/commit_msg.txt
|
||||
echo "$SUBJECT_LINE" > "$COMMIT_MSG_FILE"
|
||||
#To arbitrarily extend the validatability of the timestamps in the repository even beyond the time when CAs cease to provide
|
||||
#revocation status, each timestamp commit ensures to contain CRL data of the timestamps in the previous timestamp commit.
|
||||
#In doing so, the lifetime of the timestamps can be arbitrarily extended into the future, so long as there is still at least one
|
||||
#newer timestamp in the repository which is still validatable (TSA CAs are often obliged to provide revocation status for many
|
||||
#years after the expiration of the signature certificate. Thus, so long as a new commit is added within this period, all older
|
||||
#timestamps' lifetime will get extended to the lifetime of the new timestamp).
|
||||
|
||||
# recursive function to walk the commit history (including splits of merge commits) and downloading CRL data for the first
|
||||
# timestamp commit found in every branch of the history
|
||||
# param1: hash of parent commit from where to start the search (including this commit)
|
||||
retrieve_crl_for_most_recent_parent_timestamps() {
|
||||
local COMMIT_HASH="$1"
|
||||
log "retrieve_crl_for_most_recent_parent_timestamps for $COMMIT_HASH"
|
||||
|
||||
local TIMESTAMP_COMMIT_VERSION
|
||||
local URL_ARRAY
|
||||
local TOKEN_ARRAY
|
||||
if ! extract_token_from_commit "$COMMIT_HASH" "$TMP_DIR" TIMESTAMP_COMMIT_VERSION URL_ARRAY TOKEN_ARRAY; then
|
||||
return 1
|
||||
fi
|
||||
local NUM_EXTRACTED="${#TOKEN_ARRAY[@]}"
|
||||
|
||||
if [ $NUM_EXTRACTED -eq 0 ]; then
|
||||
#this is not a timestamp commit, proceed with parent(s)
|
||||
local PARENTS=$(git cat-file -p "$COMMIT_HASH" | awk '/^$/{exit} /parent/ {print}' | sed 's/parent //')
|
||||
local RETURN_VAL=0
|
||||
if [ ! -z "$PARENTS" ]; then
|
||||
while read PARENT_HASH; do
|
||||
if ! retrieve_crl_for_most_recent_parent_timestamps "$PARENT_HASH"; then
|
||||
RETURN_VAL=1
|
||||
fi
|
||||
done <<< $(printf "%s" "$PARENTS")
|
||||
fi
|
||||
return "$RETURN_VAL"
|
||||
fi
|
||||
|
||||
#iterate over extracted token and download CRL data
|
||||
for (( i=0; i<"$NUM_EXTRACTED"; i++)); do
|
||||
local TOKEN_FILE="${TOKEN_ARRAY[$i]}"
|
||||
local DIGEST
|
||||
get_token_digest "$TOKEN_FILE" DIGEST
|
||||
local SIGNING_CERT_ID
|
||||
get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID
|
||||
#get certificate chain of this token from LTV data
|
||||
local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer
|
||||
if [ ! -f "$CERT_CHAIN_FILE" ]; then
|
||||
#If LTV data is not in the working directory, check it out from the corresponding commit
|
||||
local TMP_CERT_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".cer
|
||||
local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CERT_CHAIN_FILE")
|
||||
local CERT_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && printf "%s" "$CERT_CHAIN_CONTENT" > "$TMP_CERT_CHAIN_FILE"
|
||||
CERT_CHAIN_FILE="$TMP_CERT_CHAIN_FILE"
|
||||
fi
|
||||
#download CRL file
|
||||
local CRL_CHAIN_FILE="$TMP_LTV_DIR"/crls/"$SIGNING_CERT_ID".crl
|
||||
if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$CRL_CHAIN_FILE"; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
#get hash of unstamped commit
|
||||
DIGEST=$(git rev-parse --verify HEAD)
|
||||
#request a timestamp token for each TSA defined
|
||||
TSA_IDX=-1
|
||||
while : ; do
|
||||
((TSA_IDX++))
|
||||
TSA_URL=$(git config timestamping.tsa"$TSA_IDX".url)
|
||||
if [ -z "$TSA_URL" ]; then
|
||||
PEARENT_COMMIT_HASH=$(git rev-parse --verify HEAD)
|
||||
if ! retrieve_crl_for_most_recent_parent_timestamps "$PEARENT_COMMIT_HASH"; then
|
||||
echo_error "ERROR: Failed to download CRLs for last timestamp commit."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#In most cases the LTV data downloaded at this point won't change. However, if a new TSA
|
||||
#is being used or if a TSA changed its signing certificate since the previous timestamp commit,
|
||||
#additional LTV data needs to be added to the index, which will change the DIGEST that is signed.
|
||||
#Thus, retrieving the tokens must sometimes be repeated once.
|
||||
|
||||
DIGEST_TO_TIMESTAMP=''
|
||||
|
||||
for ((i=0; i<2; i++)); do
|
||||
|
||||
#add all ltv files
|
||||
ls "$TMP_LTV_DIR"/*/* | while read SOURCE_FILE; do
|
||||
TARGET_FILE="$LTV_DIR"${SOURCE_FILE#"$TMP_LTV_DIR"}
|
||||
cp -f "$SOURCE_FILE" "$TARGET_FILE"
|
||||
git add "$TARGET_FILE"
|
||||
done
|
||||
TREE_HASH=$(git write-tree)
|
||||
|
||||
declare PREIMAGE
|
||||
get_preimage_from_tree_and_parent "$TREE_HASH" "$PEARENT_COMMIT_HASH" PREIMAGE
|
||||
declare NEW_DIGEST_TO_TIMESTAMP
|
||||
compute_digest_from_tree_and_parent "$TREE_HASH" "$PEARENT_COMMIT_HASH" NEW_DIGEST_TO_TIMESTAMP
|
||||
if [ "$NEW_DIGEST_TO_TIMESTAMP" == "$DIGEST_TO_TIMESTAMP" ];then
|
||||
#no new LTV data added, no need to re-request token.
|
||||
break
|
||||
fi
|
||||
echo_info "for TSA $TSA_URL"
|
||||
TOKEN_OPTIONAL=$(git config --type=bool timestamping.tsa"$TSA_IDX".optional)
|
||||
#retrieve token
|
||||
TOKEN_FILE="$TMP_DIR"/token.tst
|
||||
if ! request_token "$TSA_URL" "$DIGEST" false "$TOKEN_FILE"; then
|
||||
if [ ! "$TOKEN_OPTIONAL" ]; then
|
||||
echo_error "Error: Retrieving timestamp token for critical TSA$TSA_IDX failed."
|
||||
exit 1
|
||||
else
|
||||
echo_warning "Warning: Retrieving timestamp token for optional TSA$TSA_IDX failed. Token won't be added."
|
||||
continue
|
||||
|
||||
#assert that this line is never reached in the second loop
|
||||
assert "[ $i -eq 0 ]" "in second iteration there must be no new LTV data."
|
||||
|
||||
DIGEST_TO_TIMESTAMP="$NEW_DIGEST_TO_TIMESTAMP"
|
||||
|
||||
#prepare commit message
|
||||
COMMIT_MSG_FILE="$TMP_DIR"/commit_msg.txt
|
||||
printf '%s\n' "$SUBJECT_LINE" > "$COMMIT_MSG_FILE"
|
||||
printf '\n%s %s\n' "$TRAILER_TOKEN_VERSION" "$TIMESTAMPING_VERSION" >> "$COMMIT_MSG_FILE"
|
||||
printf '\n%s %s\n' "$TRAILER_TOKEN_ALGO" "$ALGO" >> "$COMMIT_MSG_FILE"
|
||||
printf '\n%s %s\n' "$TRAILER_TOKEN_PREIMAGE" "$PREIMAGE" >> "$COMMIT_MSG_FILE"
|
||||
printf '\n%s %s\n' "$TRAILER_TOKEN_DIGEST" "$DIGEST_TO_TIMESTAMP" >> "$COMMIT_MSG_FILE"
|
||||
|
||||
#request a timestamp token for each TSA defined
|
||||
TSA_IDX=-1
|
||||
while : ; do
|
||||
((TSA_IDX++))
|
||||
TSA_URL=$(git config timestamping.tsa"$TSA_IDX".url)
|
||||
if [ -z "$TSA_URL" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
#validate token and download LTV data
|
||||
if ! verify_token_and_add_ltv_data "$TOKEN_FILE" "$DIGEST" "$TSA_URL"; then
|
||||
if [ ! "$TOKEN_OPTIONAL" ]; then
|
||||
echo_error "Error: Validating timestamp token for critical TSA$TSA_IDX failed."
|
||||
exit 1
|
||||
else
|
||||
echo_warning "Warning: Validating timestamp token for optional TSA$TSA_IDX failed. Token won't be added."
|
||||
continue
|
||||
echo_info "for TSA $TSA_URL"
|
||||
TOKEN_OPTIONAL=$(git config --type=bool timestamping.tsa"$TSA_IDX".optional)
|
||||
#retrieve token
|
||||
TOKEN_FILE="$TMP_DIR"/token.tst
|
||||
if ! request_token "$TSA_URL" "$DIGEST_TO_TIMESTAMP" false "$TOKEN_FILE"; then
|
||||
if [ ! "$TOKEN_OPTIONAL" ]; then
|
||||
echo_error "Error: Retrieving timestamp token for critical TSA$TSA_IDX failed."
|
||||
exit 1
|
||||
else
|
||||
echo_warning "Warning: Retrieving timestamp token for optional TSA$TSA_IDX failed. Token won't be added."
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
#add token to commit message
|
||||
openssl ts -reply -token_in -in "$TOKEN_FILE" -token_out -text -out "$TMP_DIR"/token.txt &> "$OUT_STREAM"
|
||||
#do not remove or change Info line (see license)
|
||||
INFO="Info: Timestamp generated with GitTrustedTimestamps by Mabulous GmbH"
|
||||
TOKENBASE64=$(openssl base64 -in "$TOKEN_FILE")
|
||||
TOKENTEXT=$(cat "$TMP_DIR"/token.txt)
|
||||
TRAILER_VALUE="$TSA_URL"$'\n'"$INFO"$'\n\n'"$TOKENTEXT"$'\n\n'"$TOKEN_HEADER"$'\n'"$TOKENBASE64"$'\n'"$TOKEN_FOOTER"
|
||||
#fold
|
||||
TRAILER_VALUE=$(echo -n "$TRAILER_VALUE" | sed -e 's/^/ /')
|
||||
git interpret-trailers --where end --if-exists addIfDifferent --no-divider --trailer "$TRAILER_TOKEN$TRAILER_VALUE" --in-place "$COMMIT_MSG_FILE"
|
||||
done
|
||||
|
||||
#add all ltv files
|
||||
if [ ! -d "$LTV_DIR"/certs ]; then
|
||||
mkdir -p "$LTV_DIR"/certs
|
||||
fi
|
||||
if [ ! -d "$LTV_DIR"/crls ]; then
|
||||
mkdir -p "$LTV_DIR"/crls
|
||||
fi
|
||||
ls "$TMP_LTV_DIR"/*/* | while read SOURCE_FILE; do
|
||||
TARGET_FILE="$LTV_DIR"${SOURCE_FILE#"$TMP_LTV_DIR"}
|
||||
mv -f "$SOURCE_FILE" "$TARGET_FILE"
|
||||
git add "$TARGET_FILE"
|
||||
#validate token and download LTV data
|
||||
if ! verify_token_and_add_ltv_data "$TOKEN_FILE" "$DIGEST_TO_TIMESTAMP" "$TSA_URL"; then
|
||||
if [ ! "$TOKEN_OPTIONAL" ]; then
|
||||
echo_error "Error: Validating timestamp token for critical TSA$TSA_IDX failed."
|
||||
exit 1
|
||||
else
|
||||
echo_warning "Warning: Validating timestamp token for optional TSA$TSA_IDX failed. Token won't be added."
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
#add token to commit message
|
||||
openssl ts -reply -token_in -in "$TOKEN_FILE" -token_out -text -out "$TMP_DIR"/token.txt &> "$OUT_STREAM"
|
||||
#do not remove or change Info line (see license)
|
||||
INFO="Info: Timestamp generated with GitTrustedTimestamps by Mabulous GmbH"
|
||||
TOKENBASE64=$(openssl base64 -in "$TOKEN_FILE")
|
||||
TOKENTEXT=$(cat "$TMP_DIR"/token.txt)
|
||||
TRAILER_VALUE="$TSA_URL"$'\n'"$INFO"$'\n\n'"$TOKENTEXT"$'\n\n'"$TOKEN_HEADER"$'\n'"$TOKENBASE64"$'\n'"$TOKEN_FOOTER"
|
||||
#fold
|
||||
TRAILER_VALUE=$(printf "%s" "$TRAILER_VALUE" | sed -e 's/^/ /')
|
||||
printf '\n%s%s\n' "$TRAILER_TOKEN_TIMESTAMP" "$TRAILER_VALUE" >> "$COMMIT_MSG_FILE"
|
||||
done
|
||||
done
|
||||
|
||||
#commit timestamps
|
||||
git commit --allow-empty --quiet -F "$COMMIT_MSG_FILE"
|
||||
|
||||
echo_info "Timestamping complete"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -50,182 +50,179 @@ if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
|
||||
validate_commit() {
|
||||
local COMMIT_HASH="$1"
|
||||
log "validate_commit for $COMMIT_HASH"
|
||||
|
||||
local COMMIT_CONTENT=$(git cat-file -p "$COMMIT_HASH")
|
||||
if [ -z "$COMMIT_CONTENT" ]; then
|
||||
echo_error "Invalid commit hash $COMMIT_HASH"
|
||||
|
||||
local TIMESTAMP_COMMIT_VERSION
|
||||
local URL_ARRAY
|
||||
local TOKEN_ARRAY
|
||||
if ! extract_token_from_commit "$COMMIT_HASH" "$TMP_DIR" TIMESTAMP_COMMIT_VERSION URL_ARRAY TOKEN_ARRAY; then
|
||||
return 1
|
||||
fi
|
||||
#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}
|
||||
URL_START=$(( $URL_START + 2))
|
||||
local NUM_EXTRACTED=$(echo -n "$COMMIT_CONTENT" | awk '$0~trailertoken{ i++; print substr($0,"$URL_START") > tmpdir i ".extracted_token.url" } /-----END/{show=0} show{print substr($0,2) > tmpdir i ".extracted_token.pem"} /-----BEGIN/{show=1} END {print i}' tmpdir="$TMP_DIR/" trailertoken="$TRAILER_TOKEN")
|
||||
if [ -z "$NUM_EXTRACTED" ]; then
|
||||
NUM_EXTRACTED=0
|
||||
local NUM_EXTRACTED="${#TOKEN_ARRAY[@]}"
|
||||
|
||||
if [ $NUM_EXTRACTED -eq 0 ];then
|
||||
#this is not a timestamp commit
|
||||
return 0
|
||||
fi
|
||||
#iterate over extracted base64 encodings, convert to DER, if it is a timestamp token, evaluate it.
|
||||
|
||||
local PARENT_HASH=$(git cat-file -p "$COMMIT_HASH" | awk '/^$/{exit} /parent/ {print}' | sed 's/parent //')
|
||||
local TREE_HASH=$(git cat-file -p "$COMMIT_HASH" | awk '/^$/{exit} /tree/ {print}' | sed 's/tree //')
|
||||
local EXPECTED_DIGEST
|
||||
if [ $TIMESTAMP_COMMIT_VERSION -eq 0 ]; then
|
||||
# version 0 timestamped directly the parent hash
|
||||
EXPECTED_DIGEST="$PARENT_HASH"
|
||||
else
|
||||
# later versions add LTV data for last timestamp in the timestamp commit, so the tree is part of the digest
|
||||
compute_digest_from_tree_and_parent "$TREE_HASH" "$PARENT_HASH" EXPECTED_DIGEST
|
||||
fi
|
||||
|
||||
#iterate over extracted token
|
||||
local ERROR_INFO_FILE="$TMP_DIR"/error_info.txt
|
||||
local ERROR_INFO=""
|
||||
local TMP_DER="$TMP_DIR"/extracted_token.der
|
||||
local TMP_TOKEN="$TMP_DIR"/extracted_token.tst
|
||||
local NUM_VALID=0
|
||||
local NUM_INVALID=0
|
||||
local NUM_SKIPPED=0
|
||||
local PARENT_HASH=$(git cat-file -p "$COMMIT_HASH" | awk '/^$/{exit} /parent/ {print}' | sed 's/parent //')
|
||||
while ls "$TMP_DIR"/*.extracted_token.pem &> "$OUT_STREAM" && read EXTRACTED_TOKEN_PEM; do
|
||||
openssl base64 -d -in "$EXTRACTED_TOKEN_PEM" -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 "$TMP_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."
|
||||
((NUM_SKIPPED++))
|
||||
local EARLIEST_VALID_UNIX_TIME=-1
|
||||
for (( i=0; i<"$NUM_EXTRACTED"; i++)); do
|
||||
local TMP_TOKEN="${TOKEN_ARRAY[$i]}"
|
||||
local DIGEST
|
||||
get_token_digest "$TMP_TOKEN" DIGEST
|
||||
if [ "${DIGEST,,}" != "${EXPECTED_DIGEST,,}" ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid because the contained digest $DIGEST does not match the timestamped hash $EXPECTED_DIGEST"
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
else
|
||||
#this is a token, validate it!
|
||||
local DIGEST=''
|
||||
get_token_digest "$TMP_TOKEN" DIGEST
|
||||
if [ "${DIGEST,,}" != "${PARENT_HASH,,}" ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid because the contained digest $DIGEST does not match the timestamped hash $PARENT_HASH"
|
||||
fi
|
||||
local SIGNING_CERT_ID=''
|
||||
get_tsa_cert_id "$TMP_TOKEN" SIGNING_CERT_ID
|
||||
local TOKEN_UNIXTIME=''
|
||||
get_token_unix_time "$TMP_TOKEN" TOKEN_UNIXTIME
|
||||
local TSA_URL="${URL_ARRAY[$i]}"
|
||||
local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer
|
||||
if [ ! -f "$CERT_CHAIN_FILE" ]; then
|
||||
#If LTV data is not in the working directory, try to check it out from the corresponding commit
|
||||
local TMP_CERT_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".cer
|
||||
local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CERT_CHAIN_FILE")
|
||||
local CERT_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && printf "%s" "$CERT_CHAIN_CONTENT" > "$TMP_CERT_CHAIN_FILE"
|
||||
CERT_CHAIN_FILE="$TMP_CERT_CHAIN_FILE"
|
||||
fi
|
||||
if [ ! -f "$CERT_CHAIN_FILE" ]; then
|
||||
#if ltv data has not been stored for this commit, try to contact TSA to recreate it
|
||||
if ! build_certificate_chain_for_token "$TMP_TOKEN" "$DIGEST" "$TSA_URL" "$CERT_CHAIN_FILE"; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since neither LTV data of certificate chain could be found nor could the certificate chain be recreated from the TSA url."
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
local SIGNING_CERT_ID=''
|
||||
get_tsa_cert_id "$TMP_TOKEN" SIGNING_CERT_ID
|
||||
local TOKEN_UNIXTIME=''
|
||||
get_token_unix_time "$TMP_TOKEN" TOKEN_UNIXTIME
|
||||
local TSA_URL_FILE="${EXTRACTED_TOKEN_PEM/.pem/.url}"
|
||||
local TSA_URL=$(cat "$TSA_URL_FILE")
|
||||
local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer
|
||||
if [ ! -f "$CERT_CHAIN_FILE" ]; then
|
||||
#If LTV data is not in the working directory, try to check it out from the corresponding commit
|
||||
local TMP_CERT_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".cer
|
||||
local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CERT_CHAIN_FILE")
|
||||
local CERT_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && echo -n "$CERT_CHAIN_CONTENT" > "$TMP_CERT_CHAIN_FILE"
|
||||
CERT_CHAIN_FILE="$TMP_CERT_CHAIN_FILE"
|
||||
fi
|
||||
if [ ! -f "$CERT_CHAIN_FILE" ]; then
|
||||
#if ltv data has not been stored for this commit, try to contact TSA to recreate it
|
||||
if ! build_certificate_chain_for_token "$TMP_TOKEN" "$DIGEST" "$TSA_URL" "$CERT_CHAIN_FILE"; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since neither LTV data of certificate chain could be found nor could the certificate chain be recreated from the TSA url."
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
#$CERT_CHAIN_FILE at this point contains certificate chain of token's signing certificate.
|
||||
#first validate the token itself at the time of timestamping
|
||||
if ! openssl ts -verify -digest "$DIGEST" -in "$TMP_TOKEN" -token_in -attime "$TOKEN_UNIXTIME" \
|
||||
-CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since it is invalid or its rootCA isn't trusted: $ERROR_INFO"
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
#$CERT_CHAIN_FILE at this point contains certificate chain of token's signing certificate.
|
||||
#first validate the token itself at the time of timestamping
|
||||
if ! openssl ts -verify -digest "$DIGEST" -in "$TMP_TOKEN" -token_in -attime "$TOKEN_UNIXTIME" \
|
||||
-CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since it is invalid or its rootCA isn't trusted: $ERROR_INFO"
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
|
||||
#now validate the issuing certificate at the time of timestamping, using historical CRLs
|
||||
local CRL_CHAIN_FILE="$LTV_DIR"/crls/"$SIGNING_CERT_ID".crl
|
||||
local HISTORIC_CRL_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".crl
|
||||
local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CRL_CHAIN_FILE")
|
||||
local CRL_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && echo "$CRL_CHAIN_CONTENT" > "$HISTORIC_CRL_CHAIN_FILE"
|
||||
if [ ! -f "$HISTORIC_CRL_CHAIN_FILE" ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since no CRL data valid at the time of timestamping could be found."
|
||||
#now validate the issuing certificate at the time of timestamping, using historical CRLs
|
||||
local CRL_CHAIN_FILE="$LTV_DIR"/crls/"$SIGNING_CERT_ID".crl
|
||||
local HISTORIC_CRL_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".crl
|
||||
local PATH_SPEC=$(realpath --relative-to="$ROOT_DIR" "$CRL_CHAIN_FILE")
|
||||
local CRL_CHAIN_CONTENT=$(git show "$COMMIT_HASH":"$PATH_SPEC") && printf "%s" "$CRL_CHAIN_CONTENT" > "$HISTORIC_CRL_CHAIN_FILE"
|
||||
if [ ! -f "$HISTORIC_CRL_CHAIN_FILE" ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since no CRL data valid at the time of timestamping could be found."
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
#historic CRL data available, check if the signing certificate was valid at the time of timestamping
|
||||
if ! openssl verify -attime "$TOKEN_UNIXTIME" -CApath "$CA_PATH" -CRLfile "$HISTORIC_CRL_CHAIN_FILE" \
|
||||
-crl_check_all -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
cat "$HISTORIC_CRL_CHAIN_FILE"
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid since TSA certificate has not been valid at the time the timestamp was created: $ERROR_INFO"
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
|
||||
#now check that for each certificate in the trust chain a currently valid CRL can be found AND that
|
||||
#each of the certificates either hasn't been revoked OR it has been revoked and the revocation entry contains the reasonCode
|
||||
#extension and the reason code is one of unspecified (0), affiliationChanged (3), superseded (4) or cessationOfOperation (5) (see chapter 4 of https://www.ietf.org/rfc/rfc3161.txt)
|
||||
local MOST_CURRENT_CRL_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".crl
|
||||
if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$MOST_CURRENT_CRL_CHAIN_FILE"; then
|
||||
echo_warning "Current CRLs for token could not be downloaded. Will try to use most recent CRL in LTV store".
|
||||
if ! git show HEAD:"$CRL_CHAIN_FILE" > "$MOST_CURRENT_CRL_CHAIN_FILE"; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since no currently valid CRL data could be found."
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
#historic CRL data available, check if the signing certificate was valid at the time of timestamping
|
||||
if ! openssl verify -attime "$TOKEN_UNIXTIME" -CApath "$CA_PATH" -CRLfile "$HISTORIC_CRL_CHAIN_FILE" \
|
||||
-crl_check_all -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
cat "$HISTORIC_CRL_CHAIN_FILE"
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid since TSA certificate has not been valid at the time the timestamp was created: $ERROR_INFO"
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
|
||||
#now check that for each certificate in the trust chain a currently valid CRL can be found AND that
|
||||
#each of the certificates either hasn't been revoked OR it has been revoked and the revocation entry contains the reasonCode
|
||||
#extension and the reason code is one of unspecified (0), affiliationChanged (3), superseded (4) or cessationOfOperation (5) (see chapter 4 of https://www.ietf.org/rfc/rfc3161.txt)
|
||||
local MOST_CURRENT_CRL_CHAIN_FILE="$TMP_DIR"/"$SIGNING_CERT_ID".crl
|
||||
if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$MOST_CURRENT_CRL_CHAIN_FILE"; then
|
||||
echo_warning "Current CRLs for token could not be downloaded. Will try to use most recent CRL in LTV store".
|
||||
if ! git show HEAD:"$CRL_CHAIN_FILE" > "$MOST_CURRENT_CRL_CHAIN_FILE"; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since no currently valid CRL data could be found."
|
||||
((NUM_INVALID++))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
#expand cert chain and crl chain into individual files
|
||||
#remove files from previous runs
|
||||
rm -f "$TMP_DIR"/*.extracted_cert.pem
|
||||
rm -f "$TMP_DIR"/*.extracted_crl.pem
|
||||
cat "$CERT_CHAIN_FILE" \
|
||||
fi
|
||||
#expand cert chain and crl chain into individual files
|
||||
#remove files from previous runs
|
||||
rm -f "$TMP_DIR"/*.extracted_cert.pem
|
||||
rm -f "$TMP_DIR"/*.extracted_crl.pem
|
||||
cat "$CERT_CHAIN_FILE" \
|
||||
| awk '/-----BEGIN CERTIFICATE-----/ { i++; } /-----BEGIN CERTIFICATE-----/, /-----END CERTIFICATE-----/ \
|
||||
{ print > tmpdir i ".extracted_cert.pem" }' tmpdir="$TMP_DIR/"
|
||||
cat "$MOST_CURRENT_CRL_CHAIN_FILE" \
|
||||
cat "$MOST_CURRENT_CRL_CHAIN_FILE" \
|
||||
| awk '/-----BEGIN X509 CRL-----/ { i++; } /-----BEGIN X509 CRL-----/, /-----END X509 CRL-----/ \
|
||||
{ print > tmpdir i ".extracted_crl.pem" }' tmpdir="$TMP_DIR/"
|
||||
#iterate over extracted certificates (first is signing certificate, last is self-signed root)
|
||||
while ls "$TMP_DIR"/*.extracted_cert.pem &> "$OUT_STREAM" && read EXTRACTED_CERT; do
|
||||
if ! openssl verify -CApath "$CA_PATH" -CRLfile "$MOST_CURRENT_CRL_CHAIN_FILE" \
|
||||
-crl_check -untrusted "$CERT_CHAIN_FILE" "$EXTRACTED_CERT" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
local ERROR_NUMBER=$(echo -n "$ERROR_INFO" | awk '/depth lookup/;' | sed 's/error //' | sed 's/ at.*//')
|
||||
#local ERROR_DEPTH=$(cat error.txt | awk '/depth lookup/;' | sed 's/.*at //' | sed 's/ depth lookup.*//')
|
||||
#error number must be
|
||||
local X509_V_ERR_CERT_REVOKED=23
|
||||
if [ "$ERROR_NUMBER" -ne "$X509_V_ERR_CERT_REVOKED" ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since certificate validity could not be verified. Error: $ERROR_INFO."
|
||||
#iterate over extracted certificates (first is signing certificate, last is self-signed root)
|
||||
while ls "$TMP_DIR"/*.extracted_cert.pem &> "$OUT_STREAM" && read EXTRACTED_CERT; do
|
||||
if ! openssl verify -CApath "$CA_PATH" -CRLfile "$MOST_CURRENT_CRL_CHAIN_FILE" \
|
||||
-crl_check -untrusted "$CERT_CHAIN_FILE" "$EXTRACTED_CERT" 1> "$OUT_STREAM" 2> "$ERROR_INFO_FILE"; then
|
||||
ERROR_INFO=$(cat "$ERROR_INFO_FILE")
|
||||
local -i ERROR_NUMBER=$(printf "%s" "$ERROR_INFO" | awk '/depth lookup/;' | sed 's/error //' | sed 's/ at.*//')
|
||||
#local ERROR_DEPTH=$(cat error.txt | awk '/depth lookup/;' | sed 's/.*at //' | sed 's/ depth lookup.*//')
|
||||
#error number must be
|
||||
local -i X509_V_ERR_CERT_REVOKED=23
|
||||
if [ $ERROR_NUMBER -ne $X509_V_ERR_CERT_REVOKED ]; then
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH could not be validated since certificate validity could not be verified. Error: $ERROR_INFO."
|
||||
((NUM_INVALID++))
|
||||
continue 2
|
||||
else
|
||||
#find revocation reason
|
||||
local CERT_SERIAL=$(openssl x509 -inform PEM -in "$EXTRACTED_CERT" -noout -serial | sed 's/serial=//')
|
||||
local REVOCATION_ACCEPTABLE=false
|
||||
local REASON=''
|
||||
while ls "$TMP_DIR"/*.extracted_crl.pem &> "$OUT_STREAM" && read EXTRACTED_CRL; do
|
||||
REASON=$(openssl crl -inform PEM -in "$EXTRACTED_CRL" -noout -text | awk '/$"CERT_SERIAL"/{f=1; next} f && /Serial Number:/{f=0} f && /CRL Reason Code:/{g=1; next} g {print; exit}' | sed 's/ *//')
|
||||
if [ -z "$REASON" ]; then
|
||||
continue
|
||||
fi
|
||||
#acceptable reasons: see chapter 4 of RFC3161
|
||||
if [[ "$REASON" == "Unspecified" || "$REASON" == "Affiliation Changed" || "$REASON" == "Superseded" || "$REASON" == "Cessation Of Operation" ]]; then
|
||||
REVOCATION_ACCEPTABLE=true
|
||||
break
|
||||
fi
|
||||
done <<< $(ls "$TMP_DIR"/*.extracted_crl.pem 2> "$OUT_STREAM")
|
||||
if [ "$REVOCATION_ACCEPTABLE" != true ]; then
|
||||
if [ -z "$REASON" ]; then
|
||||
REASON="Certificate revoked without reasonCode extension."
|
||||
fi
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid since certificate was revoked for the following reason: $REASON"
|
||||
((NUM_INVALID++))
|
||||
continue 2
|
||||
else
|
||||
#find revocation reason
|
||||
local CERT_SERIAL=$(openssl x509 -inform PEM -in "$EXTRACTED_CERT" -noout -serial | sed 's/serial=//')
|
||||
local REVOCATION_ACCEPTABLE=false
|
||||
local REASON=''
|
||||
while ls "$TMP_DIR"/*.extracted_crl.pem &> "$OUT_STREAM" && read EXTRACTED_CRL; do
|
||||
REASON=$(openssl crl -inform PEM -in "$EXTRACTED_CRL" -noout -text | awk '/$"CERT_SERIAL"/{f=1; next} f && /Serial Number:/{f=0} f && /CRL Reason Code:/{g=1; next} g {print; exit}' | sed 's/ *//')
|
||||
if [ -z "$REASON" ]; then
|
||||
continue
|
||||
fi
|
||||
#acceptable reasons: see chapter 4 of RFC3161
|
||||
if [[ "$REASON" == "Unspecified" || "$REASON" == "Affiliation Changed" || "$REASON" == "Superseded" || "$REASON" == "Cessation Of Operation" ]]; then
|
||||
REVOCATION_ACCEPTABLE=true
|
||||
break
|
||||
fi
|
||||
done <<< $(ls "$TMP_DIR"/*.extracted_crl.pem 2> "$OUT_STREAM")
|
||||
if [ "$REVOCATION_ACCEPTABLE" != true ]; then
|
||||
if [ -z "$REASON" ]; then
|
||||
REASON="Certificate revoked without reasonCode extension."
|
||||
fi
|
||||
echo_warning "Token from $TSA_URL in commit $COMMIT_HASH is invalid since certificate was revoked for the following reason: $REASON"
|
||||
((NUM_INVALID++))
|
||||
continue 2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <<< $(ls "$TMP_DIR"/*.extracted_cert.pem 2> "$OUT_STREAM")
|
||||
#token is valid
|
||||
((NUM_VALID++))
|
||||
fi
|
||||
done <<< $(ls "$TMP_DIR"/*.extracted_cert.pem 2> "$OUT_STREAM")
|
||||
#token is valid
|
||||
if [ $EARLIEST_VALID_UNIX_TIME -eq -1 ] || [ $TOKEN_UNIXTIME -lt $EARLIEST_VALID_UNIX_TIME ];then
|
||||
EARLIEST_VALID_UNIX_TIME=$TOKEN_UNIXTIME
|
||||
fi
|
||||
done <<< $(ls "$TMP_DIR"/*.extracted_token.pem 2> "$OUT_STREAM")
|
||||
local NUM_PROCESSED=$(( $NUM_VALID + $NUM_INVALID + $NUM_SKIPPED ))
|
||||
((NUM_VALID++))
|
||||
done #for loop
|
||||
local NUM_PROCESSED=$(( $NUM_VALID + $NUM_INVALID ))
|
||||
#assert that all extracted timestamps have been processed
|
||||
if [ "$NUM_PROCESSED" -ne "$NUM_EXTRACTED" ]; then
|
||||
echo_error "Assetion failed NUM_PROCESSED==NUM_EXTRACTED"
|
||||
return 1
|
||||
fi
|
||||
if [ "$NUM_VALID" -eq 0 ] && [ "$NUM_INVALID" -eq 0 ]; then
|
||||
#this commit does not contain timestamp tokens.
|
||||
return 0
|
||||
elif [ "$NUM_VALID" -gt 0 ]; then
|
||||
if [ "$NUM_INVALID" -gt 0 ]; then
|
||||
assert "[ $NUM_PROCESSED -eq $NUM_EXTRACTED ]" "All extracted token must be processed."
|
||||
|
||||
if [ $NUM_VALID -gt 0 ]; then
|
||||
if [ $NUM_INVALID -gt 0 ]; then
|
||||
echo_warning "Warning: While commit $COMMIT_HASH contains $NUM_VALID valid timestamp tokens and thus is considered proppely timestamped, it also contains $NUM_INVALID invalid timestamp tokens."
|
||||
fi
|
||||
echo_info "Commit $COMMIT_HASH contains $NUM_VALID valid timestamp tokens."
|
||||
DATE_STRING=$(date -d @"$EARLIEST_VALID_UNIX_TIME")
|
||||
echo_info "Commit $COMMIT_HASH, which timestamps commit $PARENT_HASH at $DATE_STRING, contains $NUM_VALID valid timestamp tokens."
|
||||
echo ""
|
||||
return 0
|
||||
fi
|
||||
echo_error "All $NUM_INVALID timestamp tokens in commit $COMMIT_HASH are invalid."
|
||||
echo_error "All $NUM_EXTRACTED timestamp tokens in commit $COMMIT_HASH are invalid."
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -235,7 +232,7 @@ validate_commit() {
|
||||
validate_commit_and_parents() {
|
||||
local COMMIT_HASH="$1"
|
||||
log "validate_commit_and_parents for $COMMIT_HASH"
|
||||
|
||||
|
||||
local ALL_PASSED=true
|
||||
if ! validate_commit "$COMMIT_HASH"; then
|
||||
ALL_PASSED=false
|
||||
@@ -247,7 +244,7 @@ validate_commit_and_parents() {
|
||||
if ! validate_commit_and_parents "$PARENT_HASH"; then
|
||||
ALL_PASSED=false
|
||||
fi
|
||||
done <<< $(echo -n "$PARENTS")
|
||||
done <<< $(printf "%s" "$PARENTS")
|
||||
fi
|
||||
if [ "$ALL_PASSED"=true ]; then
|
||||
return 0
|
||||
@@ -271,8 +268,11 @@ if ! git fsck --full --strict --no-progress --no-dangling "$COMMIT_HASH"; then
|
||||
echo_error "git fsck failed. This means the repository is in a corrupted state and cannot be validated. Restore corrupt files from a backup or remote repository."
|
||||
exit 1
|
||||
fi
|
||||
echo_info "Repository integrity OK"
|
||||
echo ""
|
||||
|
||||
echo_info "Validating timestamps. This may take a while..."
|
||||
echo ""
|
||||
if validate_commit_and_parents "$COMMIT_HASH"; then
|
||||
echo_info "Validation OK: All timestamped commits in the commit history of $COMMIT_HASH contain at least one valid timestamp."
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user