"""Local reproduction of the Litecoin Core 0.21.5.8 integrity experiment. Requires Python 3, GnuPG on PATH, and already downloaded files. No network requests. Usage: python verify-core-downloads.py /path/to/download-folder The folder must contain SHA256SUMS.asc, davidburkett38-key.pgp and the four package filenames below. This script tests consistency under the supplied key. It does NOT independently authenticate the key's owner. Establish that identity separately. It creates an isolated temporary keyring; it never executes a downloaded package. """ import pathlib,subprocess,tempfile,shutil,hashlib,re,json,sys NAMES=['litecoin-0.21.5.8-win64.zip','litecoin-0.21.5.8-win64-setup.exe','litecoin-0.21.5.8-osx.dmg','litecoin-0.21.5.8-x86_64-linux-gnu.tar.gz'] FINGERPRINT='D35621D53A1CC6A3456758D03620E9D387E55666' def main(): source=pathlib.Path(sys.argv[1]).resolve();gpg=shutil.which('gpg') if not gpg:raise SystemExit('Install GnuPG and put gpg on PATH first.') with tempfile.TemporaryDirectory(prefix='ltc-download-check-') as tmp: work=pathlib.Path(tmp);(work/'keys').mkdir() for n in ['SHA256SUMS.asc','davidburkett38-key.pgp']:shutil.copy2(source/n,work/n) def run(*args):return subprocess.run([gpg,'--homedir','keys','--batch',*args],cwd=work,capture_output=True,text=True) inspect=run('--with-colons','--show-keys','davidburkett38-key.pgp') fingerprints=[x.split(':')[9] for x in inspect.stdout.splitlines() if x.startswith('fpr:')] if FINGERPRINT not in fingerprints:raise RuntimeError('Unexpected fingerprint; stop.') run('--import','davidburkett38-key.pgp') check=run('--status-fd','1','--verify','SHA256SUMS.asc') if check.returncode or '[GNUPG:] VALIDSIG '+FINGERPRINT not in check.stdout:raise RuntimeError('Manifest signature did not verify; stop.') manifest=(source/'SHA256SUMS.asc').read_text(encoding='ascii');rows=[] for n in NAMES: raw=(source/n).read_bytes();expected=re.search(r'^([a-f0-9]{64}) '+re.escape(n)+r'$',manifest,re.M) if not expected:raise RuntimeError('Filename absent from manifest: '+n) digest=hashlib.sha256(raw).hexdigest() if digest!=expected.group(1):raise RuntimeError('Hash mismatch: '+n) rows.append({'file':n,'bytes':len(raw),'sha256':digest,'matches':True}) altered=bytearray((source/NAMES[0]).read_bytes());offset=len(altered)//2;altered[offset]^=1 changed_digest=hashlib.sha256(altered).hexdigest();assert changed_digest!=rows[0]['sha256'] # Alter the first signed checksum digit only, preserving the existing signature. altered_manifest=manifest.replace('129f55de','029f55de',1) if altered_manifest==manifest:raise RuntimeError('Pinned tamper fixture does not match this manifest.') (work/'altered.asc').write_text(altered_manifest,encoding='ascii') bad=run('--status-fd','1','--verify','altered.asc');assert bad.returncode and '[GNUPG:] BADSIG ' in bad.stdout print(json.dumps({'signature_valid_under_supplied_key':True,'key_identity_independently_authenticated_by_script':False,'downloads':rows,'one_byte_changed_at_offset':offset,'changed_zip_sha256':changed_digest,'altered_manifest_signature_rejected':True},indent=2)) if __name__=='__main__':main()