|
| 1 | +# Copyright 2024 Shift Crypto AG |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import os |
| 16 | +import requests |
| 17 | +import gnupg |
| 18 | + |
| 19 | +# Constants |
| 20 | +GITHUB_API_URL = "https://api.github.com" |
| 21 | +REPO_OWNER = "BitBoxSwiss" |
| 22 | +REPO_NAME = "bitbox02-firmware" |
| 23 | +TAG_PREFIX = "firmware/" |
| 24 | +DOWNLOAD_DIR = "downloads" |
| 25 | + |
| 26 | +# security@shiftcrypto.ch pubkey |
| 27 | +PUB_KEY = ''' |
| 28 | +-----BEGIN PGP PUBLIC KEY BLOCK----- |
| 29 | +
|
| 30 | +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 |
| 77 | +jLZugv6bMuMLjA== |
| 78 | +=0krM |
| 79 | +-----END PGP PUBLIC KEY BLOCK----- |
| 80 | +''' |
| 81 | + |
| 82 | +# Initialize GPG |
| 83 | +gpg = gnupg.GPG() |
| 84 | + |
| 85 | +def import_public_key(pub_key): |
| 86 | + result = gpg.import_keys(pub_key) |
| 87 | + if result.count == 0: |
| 88 | + raise Exception("Failed to import public key") |
| 89 | + |
| 90 | +def download_file(url, dest_path): |
| 91 | + if os.path.isfile(dest_path): |
| 92 | + print(f"File already exists, skipping. {dest_path}") |
| 93 | + return |
| 94 | + response = requests.get(url) |
| 95 | + response.raise_for_status() |
| 96 | + with open(dest_path, 'wb') as f: |
| 97 | + f.write(response.content) |
| 98 | + |
| 99 | +def verify_signature(sig_path, file_path): |
| 100 | + with open(sig_path, 'rb') as sig_file: |
| 101 | + verified = gpg.verify_file(sig_file, file_path) |
| 102 | + if not verified: |
| 103 | + raise Exception(f"Signature verification failed for {file_path}") |
| 104 | + |
| 105 | +def download_and_verify_assets(): |
| 106 | + # Get list of releases |
| 107 | + releases_url = f"{GITHUB_API_URL}/repos/{REPO_OWNER}/{REPO_NAME}/releases" |
| 108 | + response = requests.get(releases_url) |
| 109 | + response.raise_for_status() |
| 110 | + releases = response.json() |
| 111 | + |
| 112 | + if not os.path.exists(DOWNLOAD_DIR): |
| 113 | + os.makedirs(DOWNLOAD_DIR) |
| 114 | + for release in releases: |
| 115 | + if release['tag_name'].startswith(TAG_PREFIX): |
| 116 | + sig_assets = [asset for asset in release['assets'] if asset['name'].endswith('.asc')] |
| 117 | + print(f"Found {len(sig_assets)} signatures in release {release['tag_name']}") |
| 118 | + for asset in sig_assets: |
| 119 | + asset_url = asset['browser_download_url'] |
| 120 | + asset_name = asset['name'] |
| 121 | + asset_path = os.path.join(DOWNLOAD_DIR, asset_name) |
| 122 | + |
| 123 | + print(f"Downloading {asset_name} from {asset_url}") |
| 124 | + download_file(asset_url, asset_path) |
| 125 | + |
| 126 | + binary_name = asset_name[:-4] |
| 127 | + binary_path = os.path.join(DOWNLOAD_DIR, binary_name) |
| 128 | + binary_url = asset_url[:-4] |
| 129 | + print(f"Downloading {binary_name} from {binary_url}") |
| 130 | + download_file(binary_url, binary_path) |
| 131 | + |
| 132 | + print(f"Verifying {asset_name}") |
| 133 | + verify_signature(asset_path, binary_path) |
| 134 | + |
| 135 | +if __name__ == "__main__": |
| 136 | + import_public_key(PUB_KEY) |
| 137 | + download_and_verify_assets() |
| 138 | + print("Download and verification complete") |
0 commit comments