From 84ac72f55030b56405af1165acba442f5ace38d6 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 18 Dec 2023 11:46:56 -0700 Subject: [PATCH] feat(releasing): adding SHA512 and RSA signature validation script to verify releases (#26278) --- RELEASING/README.md | 15 +++++ RELEASING/validate_this_release.sh | 54 +++++++++++++++ RELEASING/verify_release.py | 101 +++++++++++++++++++++++++++++ superset-frontend/package.json | 3 +- 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100755 RELEASING/validate_this_release.sh create mode 100755 RELEASING/verify_release.py diff --git a/RELEASING/README.md b/RELEASING/README.md index b007a8917..b0cad547c 100644 --- a/RELEASING/README.md +++ b/RELEASING/README.md @@ -388,8 +388,23 @@ The script will generate the email text that should be sent to dev@superset.apac ## Validating a release +Official instructions: https://www.apache.org/info/verification.html +We now have a handy script for anyone validating a release to use. The core of it is in this very folder, `verify_release.py`. Just make sure you have all three release files in the same directory (`{some version}.tar.gz`, `{some version}.tar.gz.asc` and `{some version}tar.gz.sha512`). Then you can pass this script the path to the `.gz` file like so: +`python verify_release.py ~/path/tp/apache-superset-{version/candidate}-source.tar.gz` + + +If all goes well, you will see this result in your terminal: +```bash +SHA-512 verified +RSA key verified +``` + +There are also additional support scripts leveraging this to make it easy for those downloading a release to test it in-situ. You can do either of the following to validate these release assets: +* `cd` into `superset-frontend` and run `npm run validate-release` +* `cd` into `RELEASES` and run `./validate_this_release.sh` + ## Publishing a successful release Upon a successful vote, you'll have to copy the folder into the non-"dev/" folder. diff --git a/RELEASING/validate_this_release.sh b/RELEASING/validate_this_release.sh new file mode 100755 index 000000000..6d2b6fca5 --- /dev/null +++ b/RELEASING/validate_this_release.sh @@ -0,0 +1,54 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +#!/bin/bash + +# Function to determine Python command +get_python_command() { + if command -v python3 &>/dev/null; then + echo "python3" + else + echo "python" + fi +} + +# Function to determine Pip command +get_pip_command() { + if command -v pip3 &>/dev/null; then + echo "pip3" + else + echo "pip" + fi +} + +PYTHON=$(get_python_command) +PIP=$(get_pip_command) + +# Get the release directory's path. If you unzip an Apache release and just run the npm script to validate the release, this will be a file name like `apache-superset-x.x.xrcx-source.tar.gz` +RELEASE_DIR_NAME="../../$(basename "$(dirname "$(pwd)")").tar.gz" + +# Install dependencies from requirements.txt if the file exists +if [ -f "path/to/requirements.txt" ]; then + echo "Installing Python dependencies..." + $PYTHON -m $PIP install -r path/to/requirements.txt +fi + +# echo $PYTHON +# echo $RELEASE_DIR_NAME + +# Run the Python script with the parent directory name as an argument +$PYTHON ../RELEASING/verify_release.py "$RELEASE_DIR_NAME" diff --git a/RELEASING/verify_release.py b/RELEASING/verify_release.py new file mode 100755 index 000000000..067154e19 --- /dev/null +++ b/RELEASING/verify_release.py @@ -0,0 +1,101 @@ +import re +import subprocess +import sys +from typing import Optional + +import requests + +# Part 1: Verify SHA512 hash - this is the same as running `shasum -a 512 {release}` and comparing it against `{release}.sha512` + + +def get_sha512_hash(filename: str) -> str: + """Run the shasum command on the file and return the SHA512 hash.""" + result = subprocess.run(["shasum", "-a", "512", filename], stdout=subprocess.PIPE) + sha512_hash = result.stdout.decode().split()[0] + return sha512_hash + + +def read_sha512_file(filename: str) -> str: + """Read the corresponding .sha512 file and process its contents.""" + sha_filename = filename + ".sha512" + with open(sha_filename) as file: + lines = file.readlines() + processed_sha = "".join(lines[1:]).replace(" ", "").replace("\n", "").lower() + return processed_sha + + +def verify_sha512(filename: str) -> str: + """Verify if the SHA512 hash of the file matches with the hash in the .sha512 file.""" + sha512_hash = get_sha512_hash(filename) + sha512_file_content = read_sha512_file(filename) + + if sha512_hash == sha512_file_content: + return "SHA verified" + else: + return "SHA failed" + + +# Part 2: Verify RSA key - this is the same as running `gpg --verify {release}.asc {release}` and comparing the RSA key and email address against the KEYS file + + +def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]: + """Run the GPG verify command and extract RSA key and email address.""" + asc_filename = filename + ".asc" + result = subprocess.run( + ["gpg", "--verify", asc_filename, filename], capture_output=True + ) + output = result.stderr.decode() + + rsa_key = re.search(r"RSA key ([0-9A-F]+)", output) + email = re.search(r'issuer "([^"]+)"', output) + + rsa_key_result = rsa_key.group(1) if rsa_key else None + email_result = email.group(1) if email else None + + # Debugging: print warnings if rsa_key or email is not found + if rsa_key_result is None: + print("Warning: No RSA key found in GPG verification output.") + if email_result is None: + print("Warning: No email address found in GPG verification output.") + + return rsa_key_result, email_result + + +def verify_rsa_key(rsa_key: str, email: Optional[str]) -> str: + """Fetch the KEYS file and verify if the RSA key and email match.""" + url = "https://downloads.apache.org/superset/KEYS" + response = requests.get(url) + if response.status_code == 200: + if rsa_key not in response.text: + return "RSA key not found on KEYS page" + + # Check if email is None or not in response.text + if email and email in response.text: + return "RSA key and email verified against Apache KEYS file" + elif email: + return "RSA key verified, but Email not found on KEYS page" + else: + return "RSA key verified, but Email not available for verification" + else: + return "Failed to fetch KEYS file" + + +def verify_sha512_and_rsa(filename: str) -> None: + """Verify SHA512 hash and RSA key.""" + sha_result = verify_sha512(filename) + print(sha_result) + + rsa_key, email = get_gpg_info(filename) + if rsa_key: + rsa_result = verify_rsa_key(rsa_key, email) + print(rsa_result) + else: + print("GPG verification failed: RSA key or email not found") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python script.py ") + else: + filename = sys.argv[1] + verify_sha512_and_rsa(filename) diff --git a/superset-frontend/package.json b/superset-frontend/package.json index d8ed6f62e..f12989cd3 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -70,7 +70,8 @@ "storybook": "cross-env NODE_ENV=development BABEL_ENV=development start-storybook -p 6006", "tdd": "cross-env NODE_ENV=test jest --watch", "test": "cross-env NODE_ENV=test jest", - "type": "tsc --noEmit" + "type": "tsc --noEmit", + "validate-release": "../RELEASING/validate_this_release.sh" }, "browserslist": [ "last 3 chrome versions",