diff --git a/.github/LICENSE b/.github/LICENSE new file mode 100644 index 0000000..c9b5304 --- /dev/null +++ b/.github/LICENSE @@ -0,0 +1,37 @@ +BSD 2-Clause License + +The code of Project Gluon may be distributed under the following terms, unless +noted otherwise in individual files or subtrees. + +Copyright (c) Project Gluon +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +OpenWrt is licensed under the terms of the GNU General Public License Version 2, +which can be found at openwrt/LICENSE after the OpenWrt repository has been +obtained. This applies to the following repositories: + + * openwrt + * packages/openwrt + * packages/routing + * packages/luci diff --git a/.github/build-meta.sh b/.github/build-meta.sh index e7f15fc..a830abd 100644 --- a/.github/build-meta.sh +++ b/.github/build-meta.sh @@ -3,6 +3,7 @@ set -euxo pipefail SCRIPT_DIR="$(dirname "$0")" +UPSTREAM_REPO_NAME="freifunk-rhein-neckar/site-ffrn" # Get Git short hash for repo at $SCRIPT_DIR GIT_SHORT_HASH="$(git -C "$SCRIPT_DIR" rev-parse --short HEAD)" @@ -159,6 +160,21 @@ if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then SIGN_MANIFEST="0" fi +# Signing should only happen when pushed to the upstream repository. +# Skip this step for the pipeline to succeed but inform the user. +if [ "${GITHUB_REPOSITORY,,}" != "${UPSTREAM_REPO_NAME,,}" ] && [ "$SIGN_MANIFEST" != "0" ]; then + SIGN_MANIFEST="0" + + echo "::warning::Skip manifest signature due to action running in fork." +fi + +# We should neither deploy in a fork, as the workflow is hard-coding our firmware-server +if [ "$GITHUB_REPOSITORY" != "$UPSTREAM_REPO_NAME" ] && [ "$DEPLOY" != "0" ]; then + DEPLOY="0" + + echo "::warning::Skip deployment due to action running in fork." +fi + # Determine Version to use RELEASE_VERSION="${RELEASE_VERSION:-$DEFAULT_RELEASE_VERSION}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f780b97..96b0c09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -198,6 +198,9 @@ jobs: runs-on: ubuntu-22.04 if: > needs.targets.outputs.targets != '[]' + permissions: + id-token: write + attestations: write steps: - uses: actions/checkout@v4 @@ -271,6 +274,14 @@ jobs: gluon-path: "gluon-gha-data/gluon" hardware-target: ${{ matrix.target }} + - name: Attest Image Build Provenance + if: ${{ needs.build-meta.outputs.create-release != '0' }} + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + "gluon-gha-data/gluon/output/images/sysupgrade/*" + "gluon-gha-data/gluon/output/images/other/*" + "gluon-gha-data/gluon/output/images/factory/*" manifest: needs: [build, build-meta, targets] @@ -547,6 +558,8 @@ jobs: github.event_name == 'push' permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/checkout@v4 @@ -590,6 +603,12 @@ jobs: gluon-gha-data/release-artifacts/build-meta.txt gluon-gha-data/release-notes.md + - name: Attest Release Artifact Build Provenance + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + gluon-gha-data/release-artifacts/* + - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: diff --git a/contrib/load-attestation.py b/contrib/load-attestation.py new file mode 100755 index 0000000..c9c0f1d --- /dev/null +++ b/contrib/load-attestation.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: MIT + +import sys +import json +import os +import requests +import base64 +import hashlib +import argparse + +DEFAULT_OWNER = "freifunk-rhein-neckar" +DEFAULT_REPO = "site-ffrn" + +def parse_arguments(): + parser = argparse.ArgumentParser(description='Load and print attestation from a GitHub API.') + parser.add_argument('-o', '--owner', help='Owner of the repository', required=False, default=DEFAULT_OWNER) + parser.add_argument('-r', '--repo', help='Repository name', required=False, default=DEFAULT_REPO) + parser.add_argument('file_path', help='Path to the attestation file') + return parser.parse_args() + +def get_file_sha256(file_path): + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + while True: + data = f.read(65536) + if not data: + break + sha256.update(data) + return sha256.hexdigest() + +def load_attestation_from_gh_api(owner, repo, file_sha256): + url = f"https://api.github.com/repos/{owner}/{repo}/attestations/sha256:{file_sha256}" + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + response = requests.get(url, headers=headers) + + if response.status_code != 200: + print(f"Failed to load attestation from {url}") + sys.exit(1) + + print("Got attestation from GitHub API") + + return response.json() + +def print_attestation(data): + dsse_envelope = data.get("attestations", [{}])[0].get("bundle", {}).get("dsseEnvelope", {}) + if "payload" not in dsse_envelope: + print("No payload in attestation") + sys.exit(1) + + decoded_dsse_payload = base64.b64decode(dsse_envelope["payload"]) + + dsse_object = json.loads(decoded_dsse_payload) + ci_filename = dsse_object["subject"][0]["name"] + ci_hashes = dsse_object["subject"][0]["digest"] + print(f"Artifact: {ci_filename}") + for hash in ci_hashes.keys(): + print(f" {hash}: {ci_hashes[hash]}") + + predicate = dsse_object["predicate"] + build_definition = predicate["buildDefinition"] + run_details = predicate["runDetails"] + + build_commit = build_definition["resolvedDependencies"][0]["digest"]["gitCommit"] + print(f" Commit: {build_commit}") + invocation_id = run_details["metadata"]["invocationId"] + print(f" Run: {invocation_id}") + +if __name__ == "__main__": + # Command: load-attestation.py [-o -r ] + args = parse_arguments() + + print(f"Fetching attestation for {args.file_path} from {args.owner}/{args.repo}") + file_hash = get_file_sha256(args.file_path) + attestation = load_attestation_from_gh_api(args.owner, args.repo, file_hash) + print_attestation(attestation)