Skip to content

Commit

Permalink
Init repository
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasbeiler committed Dec 11, 2024
0 parents commit 80a1c8e
Show file tree
Hide file tree
Showing 13 changed files with 533 additions and 0 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Actions

on:
schedule:
- cron: '0 * * * *' # Runs every hour.
# on: workflow_dispatch

jobs:
run-script:
runs-on: ubuntu-latest
env:
HETZNER_API_TOKEN: ${{ secrets.HETZNER_API_TOKEN }}
HETZNER_LOCATION: ${{ secrets.HETZNER_LOCATION }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }}
PIXEL_CODENAMES: ${{ secrets.PIXEL_CODENAMES }}
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run create_server.sh
run: |
bash ./infrastructure/hetzner/create_server.sh
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Disclaimer

This project is a work in progress and will continue to evolve toward greater robustness, simplicity, and modularity. For now, don't take the results too seriously, and keep in mind that it is crucial that you know how to interpret them.

I am NOT affiliated with the GrapheneOS Foundation. I am simply an individual who is interested in reproducible builds, particularly for this operating system that I run on my phone and will play an even larger role in my life in the future with the rumored Google Pixel Laptop. Based on everything I've observed so far, a recent Google Pixel (8th/9th gen) running GrapheneOS provides the most secure device and operating system that most people can have, so I decided to invest in reproducible builds for it myself.

Reproducible builds help users ensure that the official release images match the published source code of a given software, providing transparency and fostering trust.

This project utilizes cloud instances from Hetzner to perform the following:
- Build GrapheneOS;
- Unpack images, archives, and other special or unusual file types;
- Images, archives, and other file types containing additional nested files are unpacked iteratively until everything is extracted;
- The signatures of certain files and images are stripped to prevent them from interfering with the comparison process.
- Compare the resulting reproduced build with the official build in order to see the differences;
- Publish the diffoscope output, showcasing all the differences between files in the official builds and the reproduced builds.

The fully automated reproducibility infrastructure will be triggered when a GitHub Actions workflow (running hourly) detects a new official release in the alpha channel. Official releases that hit the alpha channel usually make their way to stable within a few hours, so it's nice to reproduce them as soon as they hit alpha.

The kernel and base OS are both compiled to ensure full OS reproducibility — no prebuilts are used, except for Vanadium and other apps (which I'll build soon). Vendor blobs are fetched directly from Google via `adevtool`, rather than from GrapheneOS repositories.

Currently, this work is limited to the Google Pixel 8 Pro, which is the device I own. However, feel free to use this project with other devices. My scripts might be a bit messy at the moment, but it's a work in progress after all. This project should work with any supported Pixel device, using parameters defined by environment variables before you run the initial script. You can run it on your own infrastructure, given the right device names and cloud provider tokens. To know how to do so, check the [technical documentation](infrastructure/hetzner/README.md).

### Results
- The diffoscope reports are available at URLs that follow this pattern:
- `https://gos-reproducibility-reports.s3.us-east-1.amazonaws.com/${PIXEL_CODENAME}-${BUILD_NUMBER}.html`.
- For example: https://gos-reproducibility-reports.s3.us-east-1.amazonaws.com/husky-2024120900.html
- The SHA512 hashes of the official builds that were tested/compared with are available at URLs that follow this pattern:
- `https://gos-reproducibility-reports.s3.us-east-1.amazonaws.com/${PIXEL_CODENAME}-${BUILD_NUMBER}.checksums`.
- For example: https://gos-reproducibility-reports.s3.us-east-1.amazonaws.com/husky-2024120900.checksums

### Pending work
- [ ] Build/reproduce Vanadium, App Store, Camera, PDF Viewer, TalkBack, GmsCompat, and Info;
- [ ] Compare incremental and factory images too, instead of just ota_update and install images;
- [ ] Consider using a cheaper instance type;
- [ ] Decide whether to continue using 7z x or switch to mounting filesystem images instead;
- Also, most things I do in the scripts could be massively improved for the sake of robustness after putting everything together.
- [ ] Save money by using an instance with IPv6 and no IPv4;
- Cloning from GitHub wouldn't work, but there are some proxies available;
- Uploading to AWS S3 wouldn't work; a workaround is required.
- [ ] Sign the artifacts before uploading so that people can verify;
- [ ] Wrap the scripts in Docker for those who want to run them locally.

For more, read the "TODO:" lines inside the code.
18 changes: 18 additions & 0 deletions infrastructure/hetzner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Running
To spin up a machine following my infrastructure scripts:
1. Set the required environment variables (see the last section);
2. Modify things as you wish;
3. Run create_server.sh. It will get the latest GrapheneOS build number and start a machine to reproduce the build.

Wait a few hours and check your bucket in order to see the reproducibility reports.

### Environment variables
Export the following variables in your shell with the appropriate values ​​for your intentions and setup. If you are running in a CI environment, set them as secret environment variables:
- `HETZNER_API_TOKEN="YOUR HETZNER API TOKEN"`
- `HETZNER_LOCATION="THE DESIRED LOCATION"`
- `AWS_BUCKET_NAME="THE NAME OF YOUR S3 BUCKET"`
- `AWS_ACCESS_KEY_ID="YOUR AWS ACCESS KEY ID WITH PROPER S3 PERMISSIONS"`
- `AWS_SECRET_ACCESS_KEY="YOUR AWS SECRET ACCESS KEY WITH PROPER S3 PERMISSIONS"`
- `AWS_DEFAULT_REGION="THE AWS REGION WHERE YOUR BUCKET IS LOCATED"`
- `AWS_BUCKET_NAME="THE AWS REGION WHERE YOUR BUCKET IS LOCATED"`
- `PIXEL_CODENAMES="THE CODENAME(S) OF YOUR PIXEL DEVICE(S) (e.g. "bluejay husky tokay")"`
50 changes: 50 additions & 0 deletions infrastructure/hetzner/cloud-config.tpl.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#cloud-config
timezone: US/Eastern
users:
- name: $NONROOT_USER
shell: /bin/bash
write_files: # I store the scripts inside /usr/local/bin for convenience as it is already part of PATH.
- encoding: b64
content: ${STARTUP_SCRIPT_B64}
owner: root:root
path: /usr/local/bin/startup_script
permissions: '0755'
- encoding: b64
content: ${DELETE_SERVER_B64}
owner: root:root
path: /usr/local/bin/delete_server
permissions: '0755'
- encoding: b64
content: ${BUILD_GOS_B64}
owner: root:root
path: /usr/local/bin/build_gos
permissions: '0755'
- encoding: b64
content: ${DETECT_DEVICE_B64}
owner: root:root
path: /usr/local/bin/detect_device
permissions: '0755'
- encoding: b64
content: ${COMPARE_GOS_B64}
owner: root:root
path: /usr/local/bin/compare_gos
permissions: '0755'
- path: /etc/profile.d/custom_common_variables.sh
content: |
export PIXEL_CODENAME=$PIXEL_CODENAME
export GOS_BUILD_NUMBER=$GOS_BUILD_NUMBER
export GOS_BUILD_DATETIME=$GOS_BUILD_DATETIME
export NONROOT_USER=$NONROOT_USER
permissions: '0755'
- path: /root/.sensitive_vars
append: true
content: |
# Variables that should be exclusive to the root user.
export HETZNER_API_TOKEN=$HETZNER_API_TOKEN
export AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
export AWS_DEFAULT_REGION=$AWS_DEFAULT_REGION
export AWS_BUCKET_NAME=$AWS_BUCKET_NAME
runcmd:
- [ /usr/local/bin/startup_script ]
57 changes: 57 additions & 0 deletions infrastructure/hetzner/create_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/bin/bash
set -euo pipefail

# ~/gos_create_server_secrets.sh exports some secrets. Useful when testing locally. Do not expose it.
# On GitHub Actions, they're already set as secret environment variables.
CURL_OUTPUT="-o /dev/null"
FORCE_REPEAT_IF_ALREADY_REPRODUCED=false
if [[ -f ~/gos_create_server_secrets.sh ]]; then
source ~/gos_create_server_secrets.sh
CURL_OUTPUT=""
FORCE_REPEAT_IF_ALREADY_REPRODUCED=true
fi

cd $(dirname "$(realpath "$0")")


# Need to export these so that envsubst can see them later.
export HETZNER_LOCATION HETZNER_API_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION AWS_BUCKET_NAME GOS_BUILD_NUMBER GOS_BUILD_DATETIME
export STARTUP_SCRIPT_B64=$(cat "../../scripts/startup_script.sh" | base64 | tr -d '\n')
export DELETE_SERVER_B64=$(cat "../../scripts/delete_server.sh" | base64 | tr -d '\n')
export BUILD_GOS_B64=$(cat "../../scripts/build_gos.sh" | base64 | tr -d '\n')
export DETECT_DEVICE_B64=$(cat "../../scripts/detect_device.sh" | base64 | tr -d '\n')
export COMPARE_GOS_B64=$(cat "../../scripts/compare_gos.sh" | base64 | tr -d '\n')
export NONROOT_USER="strcat"

if ! aws s3api head-bucket --bucket "$AWS_BUCKET_NAME" --region "$AWS_DEFAULT_REGION" >/dev/null 2>&1; then
if aws s3api create-bucket --bucket "$AWS_BUCKET_NAME" --region "$AWS_DEFAULT_REGION" >/dev/null 2>&1; then
echo "S3 bucket created successfully: ${AWS_BUCKET_NAME}"
else
echo "S3 bucket ${AWS_BUCKET_NAME} does not exist and could not be created. Check your credentials and permissions."
fi
else
echo "Good! S3 bucket ${AWS_BUCKET_NAME} exists."
fi

for PIXEL_CODENAME in $PIXEL_CODENAMES; do
read -r GOS_BUILD_NUMBER GOS_BUILD_DATETIME BUILD_ID _ < <(echo $(curl -sL "https://releases.grapheneos.org/${PIXEL_CODENAME}-alpha"))
response_status_code=$(curl -sLI -w "%{http_code}" -o /dev/null "https://${AWS_BUCKET_NAME}.s3.${AWS_DEFAULT_REGION}.amazonaws.com/${PIXEL_CODENAME}-${GOS_BUILD_NUMBER}.html")

if [[ "$response_status_code" -ge 400 && "$response_status_code" -lt 500 || $FORCE_REPEAT_IF_ALREADY_REPRODUCED == true ]]; then
export PIXEL_CODENAME GOS_BUILD_NUMBER GOS_BUILD_DATETIME
USER_DATA=$(envsubst < cloud-config.tpl.yaml | awk '{printf "%s\\n", $0}')

SERVER_ID=$(curl -sL -H "Authorization: Bearer $HETZNER_API_TOKEN" --url "https://api.hetzner.cloud/v1/servers" | jq ".servers[] | select(.labels.pixel_codename == \"${PIXEL_CODENAME}\" and .labels.gos_build_number == \"${GOS_BUILD_NUMBER}\") | .id")
[[ ! -z "${SERVER_ID}" ]] && echo "A machine for ${PIXEL_CODENAME}-${GOS_BUILD_NUMBER} already exists!" && continue

curl -sL ${CURL_OUTPUT} \
-X POST \
-H "Authorization: Bearer $HETZNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"user_data\":\"$USER_DATA\",\"image\":\"debian-12\",\"location\":\"${HETZNER_LOCATION}\",\"labels\":{\"pixel_codename\":\"$PIXEL_CODENAME\",\"gos_build_number\":\"$GOS_BUILD_NUMBER\"},\"name\":\"m-$(date +%s)\",\"public_net\":{\"enable_ipv4\":true,\"enable_ipv6\":true},\"server_type\":\"cpx51\",\"start_after_create\":true}" \
"https://api.hetzner.cloud/v1/servers"
echo "Created machine for ${PIXEL_CODENAME}-${GOS_BUILD_NUMBER}!"
else
echo "Already reproduced ${PIXEL_CODENAME}-${GOS_BUILD_NUMBER}!"
fi
done;
1 change: 1 addition & 0 deletions infrastructure/local/docker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
1 change: 1 addition & 0 deletions infrastructure/local/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Placeholder. This file will describe a Docker image to be built and provide the build and comparison environment.
1 change: 1 addition & 0 deletions infrastructure/local/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Placeholder. This file will describe Docker containers to be run in order to build and reproduce GrapheneOS. It will read environment variables from a .env file from this same directory here.
99 changes: 99 additions & 0 deletions scripts/build_gos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/bin/bash
set -eo pipefail

# Build and install payload-dumper-go. Already preparing this right here so it fails early in case it fails.
git clone https://github.com/ssut/payload-dumper-go ~/payload-dumper-go
cd ~/payload-dumper-go
go build

# Download official builds to ~/comparing/official. Already preparing this right here so it fails early in case it fails.
rm -rf ~/comparing && mkdir -p ~/comparing/official ~/comparing/reproduced ~/comparing/operation_outputs
wget -P ~/comparing/official/ https://releases.grapheneos.org/${PIXEL_CODENAME}-install-${GOS_BUILD_NUMBER}.zip
wget -P ~/comparing/official/ https://releases.grapheneos.org/${PIXEL_CODENAME}-ota_update-${GOS_BUILD_NUMBER}.zip

# Initial builder preparation.
export OFFICIAL_BUILD=true
source /usr/local/bin/detect_device
mkdir -pv ~/.ssh
curl -sL https://grapheneos.org/allowed_signers > ~/.ssh/grapheneos_allowed_signers
git config --global user.name "grapheneos"
git config --global user.email "grapheneos-build@localhost"
git config --global color.ui false

# Fetch OS source code tree.
echo "[INFO] Fetching OS tree..."
mkdir -p ~/grapheneos/grapheneos-${GOS_BUILD_NUMBER}
cd ~/grapheneos/grapheneos-${GOS_BUILD_NUMBER}
repo init --depth=1 -u https://github.com/GrapheneOS/platform_manifest.git -b refs/tags/${GOS_BUILD_NUMBER}
cd .repo/manifests
git config gpg.ssh.allowedSignersFile ~/.ssh/grapheneos_allowed_signers
git verify-tag $(git describe)
cd ../..
repo sync --fail-fast --force-sync --no-clone-bundle --no-tags

# Build kernel.
echo "[INFO] Building kernel..."
mkdir -p ~/android/kernel/${PIXEL_GENERATION_CODENAME}
cd ~/android/kernel/${PIXEL_GENERATION_CODENAME}
repo init --depth=1 -u https://github.com/GrapheneOS/kernel_manifest-${PIXEL_GENERATION_SOC_CODENAME}.git -b refs/tags/${GOS_BUILD_NUMBER}
repo sync --fail-fast --force-sync --no-clone-bundle --no-tags
${KERNEL_BUILD_COMMAND}
cd ~/grapheneos/grapheneos-${GOS_BUILD_NUMBER}
REAL_KERNEL_PREBUILTS_PATH=$(realpath device/google/${PIXEL_GENERATION_CODENAME}-kernels/*/grapheneos/)
find ${REAL_KERNEL_PREBUILTS_PATH}/ -type f ! -path '*kernel-headers/*' -delete
mv ~/android/kernel/${PIXEL_GENERATION_CODENAME}/out/${PIXEL_GENERATION_CODENAME}/dist/* ${REAL_KERNEL_PREBUILTS_PATH}
rm -rf ~/android/kernel/${PIXEL_GENERATION_CODENAME}

# Build kernel for microdroid.
echo "[INFO] Building kernel for microdroid pVMs..."
mkdir -p ~/android/kernel/6.6
cd ~/android/kernel/6.6
repo init --depth=1 -u https://github.com/GrapheneOS/kernel_manifest-6.6.git -b refs/tags/${GOS_BUILD_NUMBER}
repo sync --fail-fast --force-sync --no-clone-bundle --no-tags
tools/bazel run //common:kernel_aarch64_microdroid_dist --config=stamp --lto=full
cd ~/grapheneos/grapheneos-${GOS_BUILD_NUMBER}
mv ~/android/kernel/6.6/out/kernel_aarch64_microdroid/dist/Image packages/modules/Virtualization/guest/kernel/android15-6.6/arm64/kernel-6.6
mv ~/android/kernel/6.6/out/kernel_aarch64_microdroid/dist/* packages/modules/Virtualization/guest/kernel/android15-6.6/arm64/
rm -rf ~/android/kernel/6.6

# Prepare adevtool to fetch vendor blobs.
echo "[INFO] Preparing adevtool..."
yarnpkg install --cwd vendor/adevtool/
source build/envsetup.sh
lunch sdk_phone64_x86_64-cur-user
m aapt2 lpunpack deapexer
echo "[INFO] Downloading and placing vendor blobs..."
PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin vendor/adevtool/bin/run generate-all -d ${PIXEL_CODENAME}

# Set up build environment for building the base OS and then build it.
echo "[INFO] Building OS..."
source build/envsetup.sh
export BUILD_DATETIME="$GOS_BUILD_DATETIME"
export BUILD_NUMBER="$GOS_BUILD_NUMBER"
lunch ${PIXEL_CODENAME}-cur-user
m ${M_BUILD_PARAMS}

# Generate keys. Note that these keys are irrelevant because this build will not be used anywhere.
rm -rf keys && mkdir -p keys/${PIXEL_CODENAME}
cd keys/${PIXEL_CODENAME}
CN=$(head /dev/urandom | tr -dc A-Za-z | head -c 8)
export password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 10)
echo ${password} | ../../development/tools/make_key releasekey "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key platform "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key shared "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key media "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key networkstack "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key sdk_sandbox "/CN=${CN}/" || :
echo ${password} | ../../development/tools/make_key bluetooth "/CN=${CN}/" || :
openssl genrsa 4096 | openssl pkcs8 -topk8 -scrypt -passout pass:${password} -out avb.pem
sed -i "s/\['openssl', 'rsa',/\['openssl', 'rsa', '-passin', 'pass:${password}',/" ../../external/avb/avbtool.py # Make it prompt no password
../../external/avb/avbtool.py extract_public_key --key avb.pem --output avb_pkmd.bin
cd ../..
ssh-keygen -t ed25519 -f keys/${PIXEL_CODENAME}/id_ed25519 -N ""

# Prepare ZIP packages.
. script/finalize.sh
script/generate-release.sh ${PIXEL_CODENAME} ${BUILD_NUMBER}

# Done.
echo "[INFO] Finished building!"
Loading

0 comments on commit 80a1c8e

Please sign in to comment.