-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathtimestamping
628 lines (562 loc) · 26.2 KB
/
timestamping
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
#!/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: [email protected]
#
declare -r TMP_DIR="$(mktemp -d)"
mkdir -p "$TMP_DIR"/ltvdir/certs
mkdir -p "$TMP_DIR"/ltvdir/crls
declare -r TMP_LTV_DIR="$TMP_DIR"/ltvdir
#set exit trap to clean up temporary files
exit_trap() {
local -i EXIT_CODE="$?"
rm -rf -- "$TMP_DIR"
exit "$EXIT_CODE"
}
trap "exit_trap" EXIT
declare OUT_STREAM=/dev/null
#uncomment for verbose output
#OUT_STREAM=/dev/stdout
#echo red text
echo_error() {
local RED='\033[0;31m'
local NO_COLOR='\033[0m'
echo -e "${RED}$1${NO_COLOR}"
}
#echo yellow text
echo_warning() {
local YELLOW='\033[1;33m'
local NO_COLOR='\033[0m'
echo -e "${YELLOW}$1${NO_COLOR}"
}
#echo light blue text
echo_info() {
local LIGHT_BLUE='\033[1;34m'
local NO_COLOR='\033[0m'
echo -e "${LIGHT_BLUE}$1${NO_COLOR}"
}
#echo light green text
echo_success() {
local LIGHT_GREEN='\033[1;32m'
local NO_COLOR='\033[0m'
echo -e "${LIGHT_GREEN}$1${NO_COLOR}"
}
#echo dark gray text to OUT_STREAM
log() {
local DARK_GRAY='\033[1;30m'
local NO_COLOR='\033[0m'
echo -e "${DARK_GRAY}$1${NO_COLOR}" > "$OUT_STREAM"
}
#assertion
#param 1: condition as string
#param 2: message
assert() {
local -r CONDITION="$1"
local MESSAGE="$2"
if [ -z "$MESSAGE" ]; then
MESSAGE="$CONDITION"
fi
local -r STACK_DEPTH=${#BASH_SOURCE[@]}
local -i i
local -r BACKTRACE="for ((i=1; i<$STACK_DEPTH; i++)); do
echo_error "\"' [$i]: ${BASH_SOURCE[$i]} : ${FUNCNAME[$i]} line ${BASH_LINENO[$i-1]}'\""
done"
local -r ASSERTION="if $CONDITION; then
:
else
echo_error "\""Assertion failed: $MESSAGE"\""
echo_error "\""Backtrace:"\""
$BACKTRACE
exit 1
fi"
if ! eval "$ASSERTION"; then
echo_info "$ASSERTION"
echo_error "Assertion in ${BASH_SOURCE[0]} on line ${BASH_LINENO[0]} is malformed."
exit 1
fi
}
declare -r TOKEN_HEADER="-----BEGIN RFC3161 TOKEN-----"
declare -r TOKEN_FOOTER="-----END RFC3161 TOKEN-----"
declare -r SUBJECT_LINE="-----TIMESTAMP COMMIT-----"
declare -r -i TIMESTAMPING_VERSION=1
declare -r TRAILER_TOKEN_VERSION="Version:"
declare -r TRAILER_TOKEN_PREIMAGE="Preimage:"
declare -r TRAILER_TOKEN_ALGO="Algorithm:"
declare -r TRAILER_TOKEN_DIGEST="Digest:"
declare -r TRAILER_TOKEN_TIMESTAMP="Timestamp:"
#get hashing algorithm used by repo
declare -r ALGO=$(git rev-parse --show-object-format)
#get length of hashes of the used algorithm
declare -r -i HASH_LENGTH=$(printf "0" | openssl dgst -"$ALGO" -binary | xxd -p -c 256 | awk '{print length; exit}')
#get directory to store validation data
declare -r ROOT_DIR=$(git rev-parse --git-dir)/..
declare -r LTV_DIR="$ROOT_DIR"/.timestampltv
#get directory for trusted RootCA certificates
declare -r CA_PATH=$(git rev-parse --git-path hooks/trustanchors)
if [ ! -d "$CA_PATH" ]; then
mkdir -p "$CA_PATH"
fi
# function to assemble the string that is used to create the digest to be timestamped
# param1: hash of parent commit
# param2: hash of tree
# param2: OUT variable, the string used to compute digest
get_preimage_from_tree_and_parent() {
local -r TREE_HASH="$1"
local -r PARENT_COMMIT_HASH="$2"
local -n PREIMAGE_OUT="$3"
log "get_preimage_from_tree_and_parent for tree $TREE_HASH and commit $PARENT_COMMIT_HASH"
#perform precondition checks
assert "[ ${#TREE_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $TREE_HASH must have length $HASH_LENGTH."
assert "[ ${#PARENT_COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $PARENT_COMMIT_HASH must have length $HASH_LENGTH."
PREIMAGE_OUT="version:${TIMESTAMPING_VERSION,,},parent:${PARENT_COMMIT_HASH,,},tree:${TREE_HASH,,}"
}
# function to compute digest that is timestamped, which is a hash that depends on commit hash and tree hash
# param1: hash of parent commit
# param2: hash of tree
# param2: OUT variable, the DIGEST
compute_digest_from_tree_and_parent() {
local -r TREE_HASH="$1"
local -r PARENT_COMMIT_HASH="$2"
local -n DIGEST_OUT="$3"
log "compute_digest_from_tree_and_parent for tree $TREE_HASH and commit $PARENT_COMMIT_HASH"
#perform precondition checks
assert "[ ${#TREE_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $TREE_HASH must have length $HASH_LENGTH."
assert "[ ${#PARENT_COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $PARENT_COMMIT_HASH must have length $HASH_LENGTH."
local PREIMAGE
get_preimage_from_tree_and_parent "$TREE_HASH" "$PARENT_COMMIT_HASH" PREIMAGE
DIGEST_OUT=$(printf "%s" "$PREIMAGE" | openssl dgst -"$ALGO" -binary | xxd -p -c 256)
#perform postcondition checks
assert "[ ${#DIGEST_OUT} -eq $HASH_LENGTH ]" "Postcondition: hash $DIGEST_OUT must have length $HASH_LENGTH."
}
# function to extract timestamp token from commit. The extracted token are saved to files in the folder provided
# and are named *.extracted_token.tst where * is the (0-based) index of the token as ordered within the commit.
# param1: hash of commit to extract token from
# param2: path to dir into which the token should be saved
# param3: OUT variable containing the version of the timestamp commit (0 if no version is defined but timestamp token were found, -1 if this commit does not contain any timestamp token)
# param4: OUT array containing the tsa urls from which each timestamp was retrieved
# param5: OUT array containing the paths to the extracted timestamp tokens
extract_token_from_commit() {
local -r COMMIT_HASH="$1"
local -r TOKEN_DIR="$2"
local -n VERSION_OUT="$3"
local -n URL_ARRAY_OUT="$4"
local -n TOKEN_ARRAY_OUT="$5"
log "extract_token_from_commit $COMMIT_HASH into $TOKEN_DIR"
#perform precondition checks
assert "[ ${#COMMIT_HASH} -eq $HASH_LENGTH ]" "Precondition: hash $COMMIT_HASH must have length $HASH_LENGTH."
assert "[ -d $TOKEN_DIR ]" "Precondition: directory $TOKEN_DIR must exist."
local -r COMMIT_CONTENT=$(git cat-file -p "$COMMIT_HASH")
assert "[ ${#COMMIT_CONTENT} -gt 0 ]" "Precondition: $COMMIT_HASH must be a valid commit hash"
#remove files from possible previous runs
rm -f "$TMP_DIR"/*.extracted_token.pem
rm -f "$TMP_DIR"/*.extracted_token.url
#extract timestamp tokens
local URL_START=${#TRAILER_TOKEN_TIMESTAMP}
URL_START=$(( $URL_START + 2))
local -i NUM_EXTRACTED=$(printf "%s" "$COMMIT_CONTENT" | awk '$0~trailerregex{ i++; insidetimestamp=1; print substr($0,urlstart) > tmpdir i ".extracted_token.url" } /-----END/{insidepem=0; insidetimestamp=0} insidepem{print substr($0,2) > tmpdir i ".extracted_token.pem"} insidetimestamp && /-----BEGIN/{insidepem=1} END {print i}' tmpdir="$TMP_DIR/" trailerregex="^$TRAILER_TOKEN_TIMESTAMP" urlstart="$URL_START")
if [ -z "$NUM_EXTRACTED" ]; then
NUM_EXTRACTED=0
fi
local -r TMP_DER="$TMP_DIR"/extracted_token.der
local -i IDX=0;
local -i i
for (( i=1; i<=$NUM_EXTRACTED; i++ )); do
local EXTRACTED_PEM_FILE="$TMP_DIR"/"$i".extracted_token.pem
local EXTRACTED_TOKEN="$TOKEN_DIR"/"$IDX".extracted_token.tst
openssl base64 -d -in "$EXTRACTED_PEM_FILE" -out "$TMP_DER"
#since a commit might contain some unrelated trailer with the name "Timestamp:" that also contains PEM header and footer, non-timestamp-tokens should be skipped
if ! openssl ts -reply -token_in -token_out -in "$TMP_DER" -out "$EXTRACTED_TOKEN" &> "$OUT_STREAM"; then
echo_warning "A PEM encoded trailer labeled $TRAILER_TOKEN has been found in commit $COMMIT_HASH which does not seem to be a timestamp token. Skipping."
continue
fi
local TSA_URL=$(cat "$TMP_DIR"/"$i".extracted_token.url)
URL_ARRAY_OUT+=("$TSA_URL")
TOKEN_ARRAY_OUT+=("$EXTRACTED_TOKEN")
((IDX++))
done
local VERSION_START=${#TRAILER_TOKEN_VERSION}
VERSION_START=$(( $VERSION_START + 1))
VERSION_OUT=$(($(printf "%s" "$COMMIT_CONTENT" | awk '$0~trailerregex{print substr($0,versionstart); exit}' trailerregex="^$TRAILER_TOKEN_VERSION" versionstart="$VERSION_START")))
if [ -z "$VERSION_OUT" ]; then
VERSION_OUT=0
fi
if [ $IDX -eq 0 ]; then
VERSION_OUT=-1
fi
#perform postcondition checks
assert "[ ${#TOKEN_ARRAY_OUT[@]} -eq ${#URL_ARRAY_OUT[@]} ]" "Postcondition: Arrays must have equal length."
assert "[ $IDX -gt 0 ] || [ $VERSION_OUT -eq -1 ]" "Postcondition: Version must be -1 if it does not contain token."
assert "[ $IDX -eq 0 ] || [ $VERSION_OUT -ge 0 ]" "Postcondition: Version must be 0 or greater if token were extracted."
}
# function to extract ESSCertID or ESSCertIDv2 of TSA from token
# param1: path to token in DER encoding
# param2: OUT variable, the ID
get_tsa_cert_id() {
local TOKEN_FILE="$1"
local -n CERT_ID_OUT="$2"
log "get_tsa_cert_id for $TOKEN_FILE"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
#this works for both ESSCertID as well as ESSCerrtIDv2 since the version2 identifier is id-smime-aa-signingCertificateV2
CERT_ID_OUT=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" \
| awk '/:id-smime-aa-signingCertificate/{f=1} f && /\[HEX DUMP\]:/ {print; exit}' \
| sed 's/^.*\[HEX DUMP\]://1')
#perform postcondition checks
assert "[ ! -z $CERT_ID_OUT ]" "Postcondition: Token in file $TOKEN_FILE must contain ESSCertID or ESSCertIDv2"
}
# function to extract hashing algorithm used in the ESSCertID (always sha1) or ESSCertIDv2
# param1: path to token in DER encoding
# param2: OUT variable, the hashing algorithm string
get_cert_id_hash_agorithm() {
local TOKEN_FILE="$1"
local -n ALGO_NAME_OUT="$2"
log "get_cert_id_hash_agorithm for $TOKEN_FILE"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
local PARSED=$(openssl asn1parse -inform DER -in "$TOKEN_FILE")
if [[ "$PARSED" == *":id-smime-aa-signingCertificateV2"* ]]; then
#TODO: extract non-default hashing algorithms
ALGO_NAME_OUT="sha256"
elif [[ "$PARSED" == *":id-smime-aa-signingCertificate"* ]]; then
ALGO_NAME_OUT="sha1"
else
ALGO_NAME_OUT="unknown"
fi
#perform postcondition checks
assert "[ $ALGO_NAME_OUT != 'unknown' ]" "Postcondition: Token in file $TOKEN_FILE must contain ESSCertID or ESSCertIDv2"
}
# function to extract digest from token file
# param1: path to token in DER encoding
# param2: OUT variable, the digest
get_token_digest() {
local TOKEN_FILE="$1"
local -n DIGEST_OUT="$2"
log "get_token_digest for $TOKEN_FILE"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
local OFFSET=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" \
| awk '/:id-smime-ct-TSTInfo/{f=1} f && /\[HEX DUMP\]:/ {print; exit}' \
| sed 's/:.*//' | sed 's/^[ \t]*//')
DIGEST_OUT=$(openssl asn1parse -inform DER -in "$TOKEN_FILE" -strparse "$OFFSET" \
| awk '/\[HEX DUMP\]:/ {print; exit}' \
| sed 's/^.*\[HEX DUMP\]://1')
#perform postcondition checks
assert "[ ${#DIGEST_OUT} -eq $HASH_LENGTH ]" "Postcondition: hash $DIGEST_OUT must have length $HASH_LENGTH."
}
# function to extract unix time of timestamp from token file
# param1: path to token in DER encoding
# param2: OUT variable, the unix time
get_token_unix_time() {
local TOKEN_FILE="$1"
local -n UNIXTIME="$2"
log "get_token_unix_time for $TOKEN_FILE"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
local TOKEN_TIMESTAMP=$(openssl ts -reply -in "$TOKEN_FILE" -token_in -token_out -text 2> "$OUT_STREAM" \
| awk '/Time stamp:/{f=1} f {print; exit}' \
| sed 's/^.*Time stamp: //1')
UNIXTIME=$(date "+%s" -d "$TOKEN_TIMESTAMP")
#perform postcondition checks
assert "[ ! -z $UNIXTIME ]" "Postcondition: Token in file $TOKEN_FILE must contain valid timestamp"
}
#function to request a timestamp for a specified digest
# param1: tsa url
# param2: digest
# param3: whether to request certificates to be included (true or false)
# param4: the file to output the token to
request_token() {
local TSA_URL="$1"
local DIGEST="$2"
local REQUEST_CERTS="$3"
local OUTPUT_FILE="$4"
log "request_token for digest $DIGEST from url $TSA_URL. REQUEST_CERTS=$REQUEST_CERTS"
#perform precondition checks
assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH."
local CONTENT_TYPE="Content-Type: application/timestamp-query"
local ACCEPT_TYPE="Accept: application/timestamp-reply"
local REQ_FILE="$TMP_DIR"/token_req.tsq
if [ "$REQUEST_CERTS" = true ]; then
if ! openssl ts -query -cert -digest "$DIGEST" -"$ALGO" -out "$REQ_FILE" &> "$OUT_STREAM"; then
echo "Error: Failed to create token query"
return 1
fi
else
if ! openssl ts -query -digest "$DIGEST" -"$ALGO" -out "$REQ_FILE" &> "$OUT_STREAM"; then
echo "Error: Failed to create token query"
return 1
fi
fi
local RESPONSE_FILE="$TMP_DIR"/response.tsr
if ! curl "$TSA_URL" -H "$CONTENT_TYPE" -H "$ACCEPT_TYPE" --data-binary @"$REQ_FILE" --output "$RESPONSE_FILE" &> "$OUT_STREAM"; then
echo "Error: Failed to get response from $TSA_URL"
return 1
fi
local RESPONSE_STATUS=$(openssl ts -reply -in "$RESPONSE_FILE" -text 2> "$OUT_STREAM" | awk '/Status: /{print; exit}' | sed 's/Status: //' | sed 's/\.//')
if [ "$RESPONSE_STATUS" != "Granted" ]; then
echo "Error: Token request was not granted."
if [ -z "$RESPONSE_STATUS" ]; then
cat "$RESPONSE_FILE"
echo ""
else
local STATUS_INFO=$(openssl ts -reply -in "$RESPONSE_FILE" -text 2> "$OUT_STREAM" | awk '/Status info:/{f=1} f {print} /Failure info: /{exit}')
echo "$STATUS_INFO"
echo "Note: If rejection reason is unrecognized or unsupported algorithm, then this tsa cannot be used for this repository, since it uses --object-format=$ALGO"
echo "The token request was:"
openssl ts -query -in "$REQ_FILE" -text 2> "$OUT_STREAM"
fi
return 1
fi
if ! openssl ts -reply -in "$RESPONSE_FILE" -token_out -out "$OUTPUT_FILE" &> "$OUT_STREAM"; then
echo "Error: Not a valid TSA response in file $RESPONSE_FILE"
return 1
fi
}
#builds a certificate chain for token. The passed token must have been requested with -cert option
# and with matching digest.
# param1: token file. Token must have been requested with -cert option and with digest of param2
# param2: the digest the token was requested for
# param3: the tsa url from which the token was requested
# param4: the output file for the chain. It contains all certificates in order, with the first
# one being the TSA cetificate and the last one the self-signed root certificate.
build_certificate_chain_for_token() {
local TOKEN_FILE="$1"
local DIGEST="$2"
local TSA_URL="$3"
local OUT_CERT_FILE="$4"
log "build_certificate_chain_for_token for token $TOKEN_FILE from $TSA_URL with digest $DIGEST and store in certificate file $OUT_CERT_FILE"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH."
local DUMMY_TOKEN="$TMP_DIR"/dummy_token.tst
local ALL_EXTRACTED_CERTS="$TMP_DIR"/extracted_certs.pem
local CHAIN=()
#if the TSA uses multiple certificates to sign tokens it may take a few attempts to get one containint the proper signer
#TODO: allow to set maximum retry attempts
local SIGNING_CERT_ID=""
get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID
local CERT_ID_HASH_ALGO=""
get_cert_id_hash_agorithm "$TOKEN_FILE" CERT_ID_HASH_ALGO
local -i i
for i in {1..10} ;do
#request dummy token. Use current commit digest
request_token "$TSA_URL" "$DIGEST" true "$DUMMY_TOKEN"
#extract certifcates
openssl pkcs7 -inform DER -in "$DUMMY_TOKEN" -print_certs -outform PEM -out "$ALL_EXTRACTED_CERTS" &> "$OUT_STREAM"
#remove files from previous runs
rm -f "$TMP_DIR"/*.extracted.pem.cer
rm -f "$TMP_DIR"/cert_chain_*.pem.cer
#extract all individual certificates from ALL_EXTRACTED_CERTS
cat "$ALL_EXTRACTED_CERTS" \
| awk '/-----BEGIN CERTIFICATE-----/ { i++; } /-----BEGIN CERTIFICATE-----/, /-----END CERTIFICATE-----/ \
{ print > tmpdir i ".extracted.pem.cer" }' tmpdir="$TMP_DIR/"
#find cetificate that signed token
local EXTRACTED_CERT
while read EXTRACTED_CERT; do
local CERT_ID=$(openssl x509 -inform PEM -in "$EXTRACTED_CERT" -outform DER | openssl dgst -"$CERT_ID_HASH_ALGO" -binary | xxd -p -c 256)
#if openssl ts -verify -digest "$DIGEST" -in "$TOKEN_FILE" -token_in -partial_chain -CAfile "$EXTRACTED_CERT" &> "$OUT_STREAM"; then
if [ "${SIGNING_CERT_ID,,}" == "${CERT_ID,,}" ]; then
#found the signer certificate
CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer)
mv -f "$EXTRACTED_CERT" "${CHAIN[-1]}"
break 2
fi
done <<< $(ls "$TMP_DIR"/*.extracted.pem.cer)
done
if [ ${#CHAIN[@]} -eq 0 ]; then
echo "Unable to download token that contains signing cert for this token:"
openssl ts -reply -token_in -token_out -in "$TOKEN_FILE" -text
return 1
fi
#iterate until self-signed certificate is reached
while ! openssl verify -CAfile "${CHAIN[-1]}" "${CHAIN[-1]}" &> "$OUT_STREAM"; do
#try to find parent certificate in extracted certs
if ls "$TMP_DIR"/*.extracted.pem.cer &> "$OUT_STREAM"; then
while read EXTRACTED_CERT; do
if openssl verify -partial_chain -CAfile "$EXTRACTED_CERT" "${CHAIN[-1]}" &> "$OUT_STREAM"; then
CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer)
mv -f "$EXTRACTED_CERT" "${CHAIN[-1]}"
continue 2
fi
done <<< $(ls "$TMP_DIR"/*.extracted.pem.cer)
fi
#otherwise try to find in trust store
if ls "$CA_PATH"/*.0 &> "$OUT_STREAM"; then
local TRUSTED_CERT
while read TRUSTED_CERT; do
if openssl verify -partial_chain -CAfile "$TRUSTED_CERT" "${CHAIN[-1]}" &> "$OUT_STREAM"; then
CHAIN+=("$TRUSTED_CERT")
continue 2
fi
done <<< $(ls "$CA_PATH"/*.0)
fi
#otherwise try to download
local URL=$(openssl x509 -inform PEM -noout -text -in "${CHAIN[-1]}" \
| awk '/Authority Information Access:/{f=1} f && /CA Issuers - URI:/ {print; exit}' \
| sed 's/^.*CA Issuers - URI://1')
if [ -z "$URL" ]; then
echo "Certificate ${CHAIN[-1]} does not contain Authority Information Access extension with CA issuer URL. Can't build certificate chain."
return 1
fi
CHAIN+=("$TMP_DIR"/cert_chain_"${#CHAIN[@]}".pem.cer)
local TMP_DOWNLOAD="$TMP_DIR"/tmp_download.crt
if ! curl "$URL" --output "$TMP_DOWNLOAD" &> "$OUT_STREAM"; then
echo "Failed to download issuer certificate from $URL"
return 1
fi
#convert from DER to PEM if necessary
if openssl x509 -inform PEM -in "$TMP_DOWNLOAD" -noout &> "$OUT_STREAM"; then
openssl x509 -inform PEM -in "$TMP_DOWNLOAD" -outform PEM -out "${CHAIN[-1]}"
elif openssl x509 -inform DER -in "$TMP_DOWNLOAD" -noout &> "$OUT_STREAM"; then
openssl x509 -inform DER -in "$TMP_DOWNLOAD" -outform PEM -out "${CHAIN[-1]}"
else
echo "Unknown certificate file format for $URL"
return 1
fi
done
echo -n > "$OUT_CERT_FILE"
local CERT
for CERT in "${CHAIN[@]}"; do
openssl x509 -in "$CERT" -noout -subject >> "$OUT_CERT_FILE"
echo '' >> "$OUT_CERT_FILE"
openssl x509 -in "$CERT" -noout -issuer >> "$OUT_CERT_FILE"
echo '' >> "$OUT_CERT_FILE"
cat "$CERT" >> "$OUT_CERT_FILE"
echo '' >> "$OUT_CERT_FILE"
done
}
# Tries to download CRLs for the entire chain and store them together in PEM encoding in an output file.
# param1: path to the certificate chain in PEM format
# param2: path to output file
# TODO: performance of this could be improved by using OCSPs to check for changes first
download_crls_for_chain() {
local CERT_FILE="$1"
local OUTPUT_FILE="$2"
log "download_crls_for_chain for certificate file $CERT_FILE and store to $OUTPUT_FILE"
#perform precondition checks
assert "[ ! -z $CERT_FILE ]" "Precondition: Path to certificate file must not be empty."
assert "[ -f $CERT_FILE ]" "Precondition: Certificate file $CERT_FILE must exist."
echo -n > "$OUTPUT_FILE"
local CRL_TMP="$TMP_DIR"/crl_tmp.crl
#remove files from possible previous runs
rm -f "$TMP_DIR"/*.extracted.pem.cer
#extract all contained certificates into separate files
cat "$CERT_FILE" \
| awk '/-----BEGIN CERTIFICATE-----/ { i++; } /-----BEGIN CERTIFICATE-----/, /-----END CERTIFICATE-----/ \
{ print > tmpdir i ".extracted.pem.cer" }' tmpdir="$TMP_DIR/"
local NUM_EXTRACTED=$(find "$TMP_DIR" -maxdepth 1 -name "*.extracted.pem.cer" -printf '.' | wc -m)
assert "[ $NUM_EXTRACTED -gt 0 ]" "Precondition: Certificate file $CERT_FILE must contain at least one certificate in PEM format."
#iterate over certificates. Ignore self-signed certificates
local EXTRACTED_CERT
ls "$TMP_DIR"/*.extracted.pem.cer | while read EXTRACTED_CERT; do
if ! openssl verify -CAfile "$EXTRACTED_CERT" "$EXTRACTED_CERT" &> "$OUT_STREAM"; then
local URL=$(openssl x509 -inform PEM -in $EXTRACTED_CERT -text -noout \
| awk '/CRL Distribution Points:/{f=1} f && /URI:/ {print; exit}' \
| sed 's/^.*URI://1')
if curl "$URL" --output "$CRL_TMP" &> "$OUT_STREAM"; then
if openssl crl -in "$CRL_TMP" -inform DER -noout &> "$OUT_STREAM"; then
openssl crl -in "$CRL_TMP" -inform DER >> "$OUTPUT_FILE"
elif openssl crl -in "$CRL_TMP" -inform PEM -noout &> "$OUT_STREAM"; then
openssl crl -in "$CRL_TMP" -inform PEM >> "$OUTPUT_FILE"
else
echo "Unknown CRL file format for $URL"
return 1
fi
else
echo "Failed to download CRL from $URL"
return 1
fi
fi
done
}
# Check whether the file containing the certificates to verify the token are available and if not,
# request and add them to the commit.
# param1: path to token in DER encoding
# param2: digest to verify
# param3: tsa url
verify_token_and_add_ltv_data() {
local TOKEN_FILE="$1"
local DIGEST="$2"
local TSA_URL="$3"
log "verify_token_and_add_ltv_data for token $TOKEN_FILE and digest $DIGEST from url $TSA_URL"
#perform precondition checks
assert "[ ! -z $TOKEN_FILE ]" "Precondition: Path to token file must not be empty."
assert "[ -f $TOKEN_FILE ]" "Precondition: Token file $TOKEN_FILE must exist."
assert "[ ${#DIGEST} -eq $HASH_LENGTH ]" "Precondition: digest $DIGEST must have length $HASH_LENGTH."
local SIGNING_CERT_ID
get_tsa_cert_id "$TOKEN_FILE" SIGNING_CERT_ID
local CERT_CHAIN_FILE="$LTV_DIR"/certs/"$SIGNING_CERT_ID".cer
if [ ! -s "$CERT_CHAIN_FILE" ]; then
CERT_CHAIN_FILE="$TMP_LTV_DIR"/certs/"$SIGNING_CERT_ID".cer
#try to build full chain.
if ! build_certificate_chain_for_token "$TOKEN_FILE" "$DIGEST" "$TSA_URL" "$CERT_CHAIN_FILE"; then
echo "Unable to build certificate chain."
return 1
fi
if ! openssl verify --CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" &> "$OUT_STREAM"; then
echo "TSA certificate from $TSA_URL is not trusted. Check your trustanchors in $CA_PATH"
return 1
fi
fi
#verify token and download CRL data
local CRL_CHAIN_FILE="$TMP_LTV_DIR"/crls/"$SIGNING_CERT_ID.crl"
#only download CRL data if it hasn't been already in a previous step
if [ ! -s "$CRL_CHAIN_FILE" ]; then
if ! download_crls_for_chain "$CERT_CHAIN_FILE" "$CRL_CHAIN_FILE"; then
echo "Could not download CRL data for $TOKEN_FILE"
return 1
fi
fi
#verify signing certificate
local TOKEN_UNIXTIME=''
get_token_unix_time "$TOKEN_FILE" TOKEN_UNIXTIME
#validate signing certificate
if ! openssl verify -attime "$TOKEN_UNIXTIME" -CApath "$CA_PATH" -CRLfile "$CRL_CHAIN_FILE" \
-crl_check_all -untrusted "$CERT_CHAIN_FILE" "$CERT_CHAIN_FILE" &> "$OUT_STREAM"; then
echo "TSA certificate from $TSA_URL could not be validated."
return 1
fi
#validate token
if ! openssl ts -verify -digest "$DIGEST" -in "$TOKEN_FILE" -token_in -attime "$TOKEN_UNIXTIME" \
-CApath "$CA_PATH" -untrusted "$CERT_CHAIN_FILE" 2> "$OUT_STREAM"; then
echo "Token from $TSA_URL could not be validated."
return 1
fi
}