Skip to content

Commit fa4b906

Browse files
committed
Merge #13066: Migrate verify-commits script to python, run in travis
e5b2cd8 Use python instead of slow shell script on verify-commits (Chun Kuan Lee) Pull request description: The cron job that runs every day would fail because of git checkout a single commit, not a branch. #12708 introduce a method to check whether merges are clean. However, there are four merges are not clean. So, I add a list of merges that are dirty and ignore them. Also, I modify the current shell script to python, it makes the script speed up a lot. The python code `tree_sha512sum` was copied from `github-merge.py` I've re-designed this. Now we verify all the things by default. - Add `--disable-tree-check` option, not to check SHA-512 tree - Add `--clean-merge NUMBER` option, only verify commits after <NUMBER> days ago Travis running time: |option|time| |-|-| |verify-commits.py|[25m47.02s(1547.02s)](https://travis-ci.org/ken2812221/bitcoin/jobs/373321423)| |verify-commits.py --disable-tree-check|[19m10.08s(1150.08s)](https://travis-ci.org/ken2812221/bitcoin/jobs/373321423)| |verify-commits.py --clean-merge 30|[9m18.18s(558.18s)](https://travis-ci.org/ken2812221/bitcoin/jobs/373321423)| |verify-commits.py --disable-tree-check --clean-merge 30|[1m16.51s(76.51s)](https://travis-ci.org/ken2812221/bitcoin/jobs/373321423)| Since the cron job always fail, I've created a respository to verify this daily. [![Build Status](https://travis-ci.org/ken2812221/bitcoin-verify-commits.svg?branch=master)](https://travis-ci.org/ken2812221/bitcoin-verify-commits) Tree-SHA512: 476bcf707d92ed3d431ca5642e013036df1506120d3dd2aa718f74240063ce856abd78f4c948336c2a6230dfe5c60c6f2d52d19bdb52d647a1c5f838eaa02e3b
2 parents ca2a233 + e5b2cd8 commit fa4b906

File tree

7 files changed

+169
-161
lines changed

7 files changed

+169
-161
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,5 @@ jobs:
104104
- test/lint/lint-all.sh
105105
- if [ "$TRAVIS_REPO_SLUG" = "bitcoin/bitcoin" -a "$TRAVIS_EVENT_TYPE" = "cron" ]; then
106106
while read LINE; do travis_retry gpg --keyserver hkp://subset.pool.sks-keyservers.net --recv-keys $LINE; done < contrib/verify-commits/trusted-keys &&
107-
travis_wait 30 contrib/verify-commits/verify-commits.sh;
107+
travis_wait 30 contrib/verify-commits/verify-commits.py;
108108
fi

contrib/verify-commits/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ are PGP signed (nearly always merge commits), as well as a script to verify
77
commits against a trusted keys list.
88

99

10-
Using verify-commits.sh safely
10+
Using verify-commits.py safely
1111
------------------------------
1212

1313
Remember that you can't use an untrusted script to verify itself. This means
14-
that checking out code, then running `verify-commits.sh` against `HEAD` is
15-
_not_ safe, because the version of `verify-commits.sh` that you just ran could
14+
that checking out code, then running `verify-commits.py` against `HEAD` is
15+
_not_ safe, because the version of `verify-commits.py` that you just ran could
1616
be backdoored. Instead, you need to use a trusted version of verify-commits
1717
prior to checkout to make sure you're checking out only code signed by trusted
1818
keys:
1919

2020
git fetch origin && \
21-
./contrib/verify-commits/verify-commits.sh origin/master && \
21+
./contrib/verify-commits/verify-commits.py origin/master && \
2222
git checkout origin/master
2323

2424
Note that the above isn't a good UI/UX yet, and needs significant improvements
@@ -42,6 +42,6 @@ said key. In order to avoid bumping the root-of-trust `trusted-git-root`
4242
file, individual commits which were signed by such a key can be added to the
4343
`allow-revsig-commits` file. That way, the PGP signatures are still verified
4444
but no new commits can be signed by any expired/revoked key. To easily build a
45-
list of commits which need to be added, verify-commits.sh can be edited to test
45+
list of commits which need to be added, verify-commits.py can be edited to test
4646
each commit with BITCOIN_VERIFY_COMMITS_ALLOW_REVSIG set to both 1 and 0, and
4747
those which need it set to 1 printed.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
f8feaa4636260b599294c7285bcf1c8b7737f74e
2+
8040ae6fc576e9504186f2ae3ff2c8125de1095c
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
6052d509105790a26b3ad5df43dd61e7f1b24a12
2+
3798e5de334c3deb5f71302b782f6b8fbd5087f1
3+
326ffed09bfcc209a2efd6a2ebc69edf6bd200b5
4+
97d83739db0631be5d4ba86af3616014652c00ec

contrib/verify-commits/pre-push-hook.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ while read LINE; do
1212
if [ "$4" != "refs/heads/master" ]; then
1313
continue
1414
fi
15-
if ! ./contrib/verify-commits/verify-commits.sh $3 > /dev/null 2>&1; then
15+
if ! ./contrib/verify-commits/verify-commits.py $3 > /dev/null 2>&1; then
1616
echo "ERROR: A commit is not signed, can't push"
17-
./contrib/verify-commits/verify-commits.sh
17+
./contrib/verify-commits/verify-commits.py
1818
exit 1
1919
fi
2020
done < /dev/stdin
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2018 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Verify commits against a trusted keys list."""
6+
import argparse
7+
import hashlib
8+
import os
9+
import subprocess
10+
import sys
11+
import time
12+
13+
GIT = os.getenv('GIT', 'git')
14+
15+
def tree_sha512sum(commit='HEAD'):
16+
"""Calculate the Tree-sha512 for the commit.
17+
18+
This is copied from github-merge.py."""
19+
20+
# request metadata for entire tree, recursively
21+
files = []
22+
blob_by_name = {}
23+
for line in subprocess.check_output([GIT, 'ls-tree', '--full-tree', '-r', commit]).splitlines():
24+
name_sep = line.index(b'\t')
25+
metadata = line[:name_sep].split() # perms, 'blob', blobid
26+
assert metadata[1] == b'blob'
27+
name = line[name_sep + 1:]
28+
files.append(name)
29+
blob_by_name[name] = metadata[2]
30+
31+
files.sort()
32+
# open connection to git-cat-file in batch mode to request data for all blobs
33+
# this is much faster than launching it per file
34+
p = subprocess.Popen([GIT, 'cat-file', '--batch'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
35+
overall = hashlib.sha512()
36+
for f in files:
37+
blob = blob_by_name[f]
38+
# request blob
39+
p.stdin.write(blob + b'\n')
40+
p.stdin.flush()
41+
# read header: blob, "blob", size
42+
reply = p.stdout.readline().split()
43+
assert reply[0] == blob and reply[1] == b'blob'
44+
size = int(reply[2])
45+
# hash the blob data
46+
intern = hashlib.sha512()
47+
ptr = 0
48+
while ptr < size:
49+
bs = min(65536, size - ptr)
50+
piece = p.stdout.read(bs)
51+
if len(piece) == bs:
52+
intern.update(piece)
53+
else:
54+
raise IOError('Premature EOF reading git cat-file output')
55+
ptr += bs
56+
dig = intern.hexdigest()
57+
assert p.stdout.read(1) == b'\n' # ignore LF that follows blob data
58+
# update overall hash with file hash
59+
overall.update(dig.encode("utf-8"))
60+
overall.update(" ".encode("utf-8"))
61+
overall.update(f)
62+
overall.update("\n".encode("utf-8"))
63+
p.stdin.close()
64+
if p.wait():
65+
raise IOError('Non-zero return value executing git cat-file')
66+
return overall.hexdigest()
67+
68+
def main():
69+
# Parse arguments
70+
parser = argparse.ArgumentParser(usage='%(prog)s [options] [commit id]')
71+
parser.add_argument('--disable-tree-check', action='store_false', dest='verify_tree', help='disable SHA-512 tree check')
72+
parser.add_argument('--clean-merge', type=float, dest='clean_merge', default=float('inf'), help='Only check clean merge after <NUMBER> days ago (default: %(default)s)', metavar='NUMBER')
73+
parser.add_argument('commit', nargs='?', default='HEAD', help='Check clean merge up to commit <commit>')
74+
args = parser.parse_args()
75+
76+
# get directory of this program and read data files
77+
dirname = os.path.dirname(os.path.abspath(__file__))
78+
print("Using verify-commits data from " + dirname)
79+
verified_root = open(dirname + "/trusted-git-root", "r").read().splitlines()[0]
80+
verified_sha512_root = open(dirname + "/trusted-sha512-root-commit", "r").read().splitlines()[0]
81+
revsig_allowed = open(dirname + "/allow-revsig-commits", "r").read().splitlines()
82+
unclean_merge_allowed = open(dirname + "/allow-unclean-merge-commits", "r").read().splitlines()
83+
incorrect_sha512_allowed = open(dirname + "/allow-incorrect-sha512-commits", "r").read().splitlines()
84+
85+
# Set commit and branch and set variables
86+
current_commit = args.commit
87+
if ' ' in current_commit:
88+
print("Commit must not contain spaces", file=sys.stderr)
89+
sys.exit(1)
90+
verify_tree = args.verify_tree
91+
no_sha1 = True
92+
prev_commit = ""
93+
initial_commit = current_commit
94+
branch = subprocess.check_output([GIT, 'show', '-s', '--format=%H', initial_commit], universal_newlines=True).splitlines()[0]
95+
96+
# Iterate through commits
97+
while True:
98+
if current_commit == verified_root:
99+
print('There is a valid path from "{}" to {} where all commits are signed!'.format(initial_commit, verified_root))
100+
sys.exit(0)
101+
if current_commit == verified_sha512_root:
102+
if verify_tree:
103+
print("All Tree-SHA512s matched up to {}".format(verified_sha512_root), file=sys.stderr)
104+
verify_tree = False
105+
no_sha1 = False
106+
107+
os.environ['BITCOIN_VERIFY_COMMITS_ALLOW_SHA1'] = "0" if no_sha1 else "1"
108+
os.environ['BITCOIN_VERIFY_COMMITS_ALLOW_REVSIG'] = "1" if current_commit in revsig_allowed else "0"
109+
110+
# Check that the commit (and parents) was signed with a trusted key
111+
if subprocess.call([GIT, '-c', 'gpg.program={}/gpg.sh'.format(dirname), 'verify-commit', current_commit], stdout=subprocess.DEVNULL):
112+
if prev_commit != "":
113+
print("No parent of {} was signed with a trusted key!".format(prev_commit), file=sys.stderr)
114+
print("Parents are:", file=sys.stderr)
115+
parents = subprocess.check_output([GIT, 'show', '-s', '--format=format:%P', prev_commit], universal_newlines=True).splitlines()[0].split(' ')
116+
for parent in parents:
117+
subprocess.call([GIT, 'show', '-s', parent], stdout=sys.stderr)
118+
else:
119+
print("{} was not signed with a trusted key!".format(current_commit), file=sys.stderr)
120+
sys.exit(1)
121+
122+
# Check the Tree-SHA512
123+
if (verify_tree or prev_commit == "") and current_commit not in incorrect_sha512_allowed:
124+
tree_hash = tree_sha512sum(current_commit)
125+
if ("Tree-SHA512: {}".format(tree_hash)) not in subprocess.check_output([GIT, 'show', '-s', '--format=format:%B', current_commit], universal_newlines=True).splitlines():
126+
print("Tree-SHA512 did not match for commit " + current_commit, file=sys.stderr)
127+
sys.exit(1)
128+
129+
# Merge commits should only have two parents
130+
parents = subprocess.check_output([GIT, 'show', '-s', '--format=format:%P', current_commit], universal_newlines=True).splitlines()[0].split(' ')
131+
if len(parents) > 2:
132+
print("Commit {} is an octopus merge".format(current_commit), file=sys.stderr)
133+
sys.exit(1)
134+
135+
# Check that the merge commit is clean
136+
commit_time = int(subprocess.check_output([GIT, 'show', '-s', '--format=format:%ct', current_commit], universal_newlines=True).splitlines()[0])
137+
check_merge = commit_time > time.time() - args.clean_merge * 24 * 60 * 60 # Only check commits in clean_merge days
138+
allow_unclean = current_commit in unclean_merge_allowed
139+
if len(parents) == 2 and check_merge and not allow_unclean:
140+
current_tree = subprocess.check_output([GIT, 'show', '--format=%T', current_commit], universal_newlines=True).splitlines()[0]
141+
subprocess.call([GIT, 'checkout', '--force', '--quiet', parents[0]])
142+
subprocess.call([GIT, 'merge', '--no-ff', '--quiet', parents[1]], stdout=subprocess.DEVNULL)
143+
recreated_tree = subprocess.check_output([GIT, 'show', '--format=format:%T', 'HEAD'], universal_newlines=True).splitlines()[0]
144+
if current_tree != recreated_tree:
145+
print("Merge commit {} is not clean".format(current_commit), file=sys.stderr)
146+
subprocess.call([GIT, 'diff', current_commit])
147+
subprocess.call([GIT, 'checkout', '--force', '--quiet', branch])
148+
sys.exit(1)
149+
subprocess.call([GIT, 'checkout', '--force', '--quiet', branch])
150+
151+
prev_commit = current_commit
152+
current_commit = parents[0]
153+
154+
if __name__ == '__main__':
155+
main()

contrib/verify-commits/verify-commits.sh

Lines changed: 0 additions & 153 deletions
This file was deleted.

0 commit comments

Comments
 (0)