Wire Forge-native release infrastructure
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s
Some checks failed
Cache: Publish Nix / Publish Nix Cache (push) Waiting to run
Build Rust / Cargo Test (push) Successful in 4m14s
Build Site / Next.js Build (push) Failing after 6s
Infra: OpenTofu / OpenTofu (grafana) (push) Successful in 5s
Lint Governance / BEP Metadata (push) Successful in 0s
Build: Android / Android Rust Core Stub (push) Failing after 23s
This commit is contained in:
parent
97c569fb35
commit
002bd382e9
199 changed files with 14268 additions and 185 deletions
78
Scripts/package/bootstrap-native-daemon.sh
Executable file
78
Scripts/package/bootstrap-native-daemon.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
apply=0
|
||||
prefer_packagekit=1
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/package/bootstrap-native-daemon.sh [--apply] [--no-packagekit]
|
||||
|
||||
Detects the host package manager and prints, or with --apply runs, the native
|
||||
install command for the Burrow daemon package. This is the host-side bootstrap
|
||||
equivalent the Flatpak GUI can point users at when PackageKit is unavailable.
|
||||
|
||||
This assumes the Burrow native repository for the detected package manager is
|
||||
already configured. Repository setup should be explicit because it establishes
|
||||
package trust roots.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply)
|
||||
apply=1
|
||||
;;
|
||||
--no-packagekit)
|
||||
prefer_packagekit=0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unknown argument: $1" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
run_or_print() {
|
||||
if [[ "$apply" == "1" ]]; then
|
||||
"$@"
|
||||
else
|
||||
printf '%q ' "$@"
|
||||
printf '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$prefer_packagekit" == "1" ]] && command -v pkcon >/dev/null 2>&1; then
|
||||
run_or_print pkcon install burrow
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
run_or_print sudo apt-get install burrow
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
run_or_print sudo dnf install burrow
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
run_or_print sudo zypper install burrow
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_or_print sudo pacman -S burrow
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_or_print sudo apk add burrow
|
||||
elif command -v xbps-install >/dev/null 2>&1; then
|
||||
run_or_print sudo xbps-install -S burrow
|
||||
elif command -v eopkg >/dev/null 2>&1; then
|
||||
run_or_print sudo eopkg install burrow
|
||||
elif command -v emerge >/dev/null 2>&1; then
|
||||
run_or_print sudo emerge --ask net-vpn/burrow
|
||||
elif command -v swupd >/dev/null 2>&1; then
|
||||
run_or_print sudo swupd bundle-add burrow
|
||||
elif command -v nix >/dev/null 2>&1; then
|
||||
run_or_print nix profile install git+https://git.burrow.net/hackclub/burrow
|
||||
else
|
||||
echo "No supported native package manager found. Install Burrow through DEB, RPM, pacman, AUR, Flatpak GUI plus daemon package, or the NixOS flake." >&2
|
||||
exit 69
|
||||
fi
|
||||
248
Scripts/package/build-repositories.sh
Executable file
248
Scripts/package/build-repositories.sh
Executable file
|
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(
|
||||
git rev-parse --show-toplevel 2>/dev/null || {
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/../.."
|
||||
pwd
|
||||
}
|
||||
)"
|
||||
|
||||
input_dir="${BURROW_PACKAGE_INPUT_DIR:-${repo_root}/dist/packages}"
|
||||
output_dir="${BURROW_PACKAGE_REPOSITORY_DIR:-${repo_root}/publish/repositories}"
|
||||
channel="${BURROW_PACKAGE_CHANNEL:-stable}"
|
||||
suite="${BURROW_APT_SUITE:-stable}"
|
||||
component="${BURROW_APT_COMPONENT:-main}"
|
||||
apt_arch="${BURROW_APT_ARCH:-${BURROW_PACKAGE_ARCH:-amd64}}"
|
||||
rpm_arch="${BURROW_RPM_ARCH:-x86_64}"
|
||||
pacman_arch="${BURROW_PACMAN_ARCH:-x86_64}"
|
||||
repo_name="${BURROW_ARCH_REPO_NAME:-burrow}"
|
||||
generic_kms_public_key_pem="${BURROW_PACKAGE_REPO_KMS_PUBLIC_KEY_PEM:-}"
|
||||
generic_kms_key="${BURROW_PACKAGE_REPO_KMS_KEY:-}"
|
||||
apt_kms_public_key_pem="${BURROW_APT_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
apt_kms_key="${BURROW_APT_REPO_KMS_KEY:-${generic_kms_key:-package-apt-repository}}"
|
||||
rpm_kms_public_key_pem="${BURROW_RPM_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
rpm_kms_key="${BURROW_RPM_REPO_KMS_KEY:-${generic_kms_key:-package-rpm-repository}}"
|
||||
pacman_kms_public_key_pem="${BURROW_PACMAN_REPO_KMS_PUBLIC_KEY_PEM:-$generic_kms_public_key_pem}"
|
||||
pacman_kms_key="${BURROW_PACMAN_REPO_KMS_KEY:-${generic_kms_key:-package-pacman-repository}}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: Scripts/package/build-repositories.sh [apt|rpm|arch|keys|all]
|
||||
|
||||
Builds native Linux package repositories from existing package artifacts.
|
||||
|
||||
Inputs:
|
||||
BURROW_PACKAGE_INPUT_DIR directory containing .deb, .rpm, and .pkg.tar.* files
|
||||
BURROW_PACKAGE_REPOSITORY_DIR output directory, default publish/repositories
|
||||
BURROW_APT_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for APT OpenPGP signatures
|
||||
BURROW_APT_REPO_KMS_KEY Google KMS key name for APT signatures
|
||||
BURROW_RPM_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for RPM repository signatures
|
||||
BURROW_RPM_REPO_KMS_KEY Google KMS key name for RPM repository signatures
|
||||
BURROW_PACMAN_REPO_KMS_PUBLIC_KEY_PEM Google KMS public key PEM for pacman signatures
|
||||
BURROW_PACMAN_REPO_KMS_KEY Google KMS key name for pacman signatures
|
||||
BURROW_PACKAGE_REPO_KMS_PUBLIC_KEY_PEM fallback public key PEM for all formats
|
||||
BURROW_PACKAGE_REPO_KMS_KEY fallback KMS key name for all formats
|
||||
BURROW_KMS_KEYRING/BURROW_KMS_LOCATION/BURROW_KMS_VERSION/GOOGLE_CLOUD_PROJECT
|
||||
|
||||
The repository metadata is signed with Google KMS-backed OpenPGP detached
|
||||
signatures when each format's KMS public key and key name are configured.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mode="${1:-all}"
|
||||
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
if command -v shasum >/dev/null 2>&1; then
|
||||
shasum -a 256 "$file" > "${file}.sha256"
|
||||
else
|
||||
sha256sum "$file" > "${file}.sha256"
|
||||
fi
|
||||
}
|
||||
|
||||
sign_file() {
|
||||
local file="$1"
|
||||
local output="${2:-${file}.sig}"
|
||||
local armor="${3:-0}"
|
||||
local kms_key="$4"
|
||||
local kms_public_key_pem="$5"
|
||||
if [[ -z "$kms_public_key_pem" || -z "$kms_key" ]]; then
|
||||
echo "warning: KMS OpenPGP signing disabled for ${file}; set the ${file} repository KMS public key and key name" >&2
|
||||
return 0
|
||||
fi
|
||||
args=(
|
||||
"${repo_root}/Scripts/package/google-kms-openpgp.py"
|
||||
detach-sign
|
||||
--public-key-pem "$kms_public_key_pem"
|
||||
--kms-key "$kms_key"
|
||||
--input "$file"
|
||||
--output "$output"
|
||||
)
|
||||
if [[ "$armor" == "1" ]]; then
|
||||
args+=(--armor)
|
||||
fi
|
||||
"${args[@]}"
|
||||
}
|
||||
|
||||
write_public_key() {
|
||||
local name="$1"
|
||||
local kms_key="$2"
|
||||
local kms_public_key_pem="$3"
|
||||
local user_id="$4"
|
||||
local output="${output_dir}/keys/${name}.asc"
|
||||
|
||||
if [[ -z "$kms_public_key_pem" || -z "$kms_key" ]]; then
|
||||
echo "warning: KMS OpenPGP public key export disabled for ${name}; set the repository KMS public key and key name" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$output")"
|
||||
"${repo_root}/Scripts/package/google-kms-openpgp.py" \
|
||||
public-key \
|
||||
--public-key-pem "$kms_public_key_pem" \
|
||||
--kms-key "$kms_key" \
|
||||
--user-id "$user_id" \
|
||||
--output "$output" \
|
||||
--armor
|
||||
}
|
||||
|
||||
write_release_file() {
|
||||
local release_path="$1"
|
||||
local dist_dir="$2"
|
||||
local now
|
||||
now="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')"
|
||||
{
|
||||
printf 'Origin: Burrow\n'
|
||||
printf 'Label: Burrow\n'
|
||||
printf 'Suite: %s\n' "$suite"
|
||||
printf 'Codename: %s\n' "$suite"
|
||||
printf 'Date: %s\n' "$now"
|
||||
printf 'Architectures: %s\n' "$apt_arch"
|
||||
printf 'Components: %s\n' "$component"
|
||||
printf 'Description: Burrow native package repository\n'
|
||||
printf 'SHA256:\n'
|
||||
(
|
||||
cd "$dist_dir"
|
||||
find . -type f ! -name Release -print | LC_ALL=C sort | while read -r file; do
|
||||
clean="${file#./}"
|
||||
sum="$(sha256sum "$clean" | awk '{print $1}')"
|
||||
size="$(wc -c < "$clean" | tr -d ' ')"
|
||||
printf ' %s %s %s\n' "$sum" "$size" "$clean"
|
||||
done
|
||||
)
|
||||
} > "$release_path"
|
||||
}
|
||||
|
||||
build_apt() {
|
||||
local root binary_dir pool_dir release_dir release_file
|
||||
root="${output_dir}/apt"
|
||||
pool_dir="${root}/pool/${component}/b/burrow"
|
||||
binary_dir="${root}/dists/${suite}/${component}/binary-${apt_arch}"
|
||||
release_dir="${root}/dists/${suite}"
|
||||
mkdir -p "$pool_dir" "$binary_dir" "$release_dir"
|
||||
|
||||
find "$input_dir" -type f -name '*.deb' -exec cp {} "$pool_dir/" \; 2>/dev/null || true
|
||||
if ! find "$pool_dir" -type f -name '*.deb' -print -quit | grep -q .; then
|
||||
echo "warning: no .deb packages found under ${input_dir}; apt repository contains README only" >&2
|
||||
printf 'Place Burrow .deb artifacts in this repository pool before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v dpkg-scanpackages >/dev/null 2>&1; then
|
||||
echo "error: dpkg-scanpackages is required to build the apt repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$root"
|
||||
dpkg-scanpackages --arch "$apt_arch" "pool/${component}" /dev/null > "dists/${suite}/${component}/binary-${apt_arch}/Packages"
|
||||
gzip -9cn "dists/${suite}/${component}/binary-${apt_arch}/Packages" > "dists/${suite}/${component}/binary-${apt_arch}/Packages.gz"
|
||||
)
|
||||
release_file="${release_dir}/Release"
|
||||
write_release_file "$release_file" "$release_dir"
|
||||
sign_file "$release_file" "${release_file}.gpg" 1 "$apt_kms_key" "$apt_kms_public_key_pem"
|
||||
sha256_file "$release_file"
|
||||
}
|
||||
|
||||
build_rpm() {
|
||||
local root repomd
|
||||
root="${output_dir}/rpm/${channel}/${rpm_arch}"
|
||||
mkdir -p "$root"
|
||||
find "$input_dir" -type f -name '*.rpm' -exec cp {} "$root/" \; 2>/dev/null || true
|
||||
if ! find "$root" -maxdepth 1 -type f -name '*.rpm' -print -quit | grep -q .; then
|
||||
echo "warning: no .rpm packages found under ${input_dir}; rpm repository contains README only" >&2
|
||||
printf 'Place Burrow .rpm artifacts in this directory before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v createrepo_c >/dev/null 2>&1; then
|
||||
echo "error: createrepo_c is required to build the rpm repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
createrepo_c "$root"
|
||||
repomd="${root}/repodata/repomd.xml"
|
||||
sign_file "$repomd" "${repomd}.asc" 1 "$rpm_kms_key" "$rpm_kms_public_key_pem"
|
||||
sha256_file "$repomd"
|
||||
}
|
||||
|
||||
build_arch() {
|
||||
local root db
|
||||
root="${output_dir}/arch/${repo_name}/${pacman_arch}"
|
||||
mkdir -p "$root"
|
||||
find "$input_dir" -type f \( -name '*.pkg.tar.zst' -o -name '*.pkg.tar.xz' -o -name '*.pkg.tar.gz' \) -exec cp {} "$root/" \; 2>/dev/null || true
|
||||
if ! find "$root" -maxdepth 1 -type f -name '*.pkg.tar.*' -print -quit | grep -q .; then
|
||||
echo "warning: no Arch packages found under ${input_dir}; pacman repository contains README only" >&2
|
||||
printf 'Place Burrow .pkg.tar.* artifacts in this directory before publishing.\n' > "${root}/README.txt"
|
||||
return 0
|
||||
fi
|
||||
if ! command -v repo-add >/dev/null 2>&1; then
|
||||
echo "error: repo-add is required to build the Arch repository" >&2
|
||||
exit 127
|
||||
fi
|
||||
(
|
||||
cd "$root"
|
||||
repo-add "${repo_name}.db.tar.gz" ./*.pkg.tar.*
|
||||
)
|
||||
db="${root}/${repo_name}.db.tar.gz"
|
||||
sign_file "$db" "${db}.sig" 0 "$pacman_kms_key" "$pacman_kms_public_key_pem"
|
||||
find "$root" -maxdepth 1 -type f -name '*.pkg.tar.*' ! -name '*.sig' -print | while read -r package; do
|
||||
sign_file "$package" "${package}.sig" 0 "$pacman_kms_key" "$pacman_kms_public_key_pem"
|
||||
done
|
||||
sha256_file "$db"
|
||||
}
|
||||
|
||||
build_keys() {
|
||||
write_public_key "burrow-package-apt-repository" "$apt_kms_key" "$apt_kms_public_key_pem" "Burrow APT Package Repository <packages@burrow.net>"
|
||||
write_public_key "burrow-package-rpm-repository" "$rpm_kms_key" "$rpm_kms_public_key_pem" "Burrow RPM Package Repository <packages@burrow.net>"
|
||||
write_public_key "burrow-package-pacman-repository" "$pacman_kms_key" "$pacman_kms_public_key_pem" "Burrow pacman Package Repository <packages@burrow.net>"
|
||||
}
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
case "$mode" in
|
||||
apt)
|
||||
build_apt
|
||||
;;
|
||||
rpm)
|
||||
build_rpm
|
||||
;;
|
||||
arch)
|
||||
build_arch
|
||||
;;
|
||||
keys)
|
||||
build_keys
|
||||
;;
|
||||
all)
|
||||
build_keys
|
||||
build_apt
|
||||
build_rpm
|
||||
build_arch
|
||||
;;
|
||||
*)
|
||||
echo "unknown repository mode: ${mode}" >&2
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
313
Scripts/package/google-kms-openpgp.py
Executable file
313
Scripts/package/google-kms-openpgp.py
Executable file
|
|
@ -0,0 +1,313 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Create OpenPGP public keys and detached signatures backed by Google KMS.
|
||||
|
||||
This intentionally implements only the narrow RSA/SHA-256 subset Burrow needs
|
||||
for Linux package repository metadata. The private key lives in Google Cloud KMS;
|
||||
this script builds OpenPGP packets locally and asks KMS to produce the RSA
|
||||
signature value.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import binascii
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RSA_PUBLIC_KEY_ALGO = 1
|
||||
SHA256_HASH_ALGO = 8
|
||||
|
||||
|
||||
def parse_der_length(data: bytes, offset: int) -> tuple[int, int]:
|
||||
first = data[offset]
|
||||
offset += 1
|
||||
if first < 0x80:
|
||||
return first, offset
|
||||
count = first & 0x7F
|
||||
if count == 0 or count > 4:
|
||||
raise ValueError("unsupported DER length")
|
||||
value = int.from_bytes(data[offset : offset + count], "big")
|
||||
return value, offset + count
|
||||
|
||||
|
||||
def parse_der_tlv(data: bytes, offset: int, expected_tag: int | None = None) -> tuple[int, bytes, int]:
|
||||
tag = data[offset]
|
||||
offset += 1
|
||||
length, offset = parse_der_length(data, offset)
|
||||
value = data[offset : offset + length]
|
||||
if len(value) != length:
|
||||
raise ValueError("truncated DER value")
|
||||
if expected_tag is not None and tag != expected_tag:
|
||||
raise ValueError(f"expected DER tag {expected_tag:#x}, got {tag:#x}")
|
||||
return tag, value, offset + length
|
||||
|
||||
|
||||
def pem_to_der(pem: bytes) -> bytes:
|
||||
lines = [
|
||||
line.strip()
|
||||
for line in pem.splitlines()
|
||||
if line and not line.startswith(b"-----")
|
||||
]
|
||||
return base64.b64decode(b"".join(lines))
|
||||
|
||||
|
||||
def parse_rsa_public_key_from_pem(path: Path) -> tuple[int, int]:
|
||||
der = pem_to_der(path.read_bytes())
|
||||
_, spki, end = parse_der_tlv(der, 0, 0x30)
|
||||
if end != len(der):
|
||||
raise ValueError("trailing data after SubjectPublicKeyInfo")
|
||||
_, _alg, offset = parse_der_tlv(spki, 0, 0x30)
|
||||
_, bit_string, offset = parse_der_tlv(spki, offset, 0x03)
|
||||
if offset != len(spki):
|
||||
raise ValueError("trailing data after SPKI bit string")
|
||||
if not bit_string or bit_string[0] != 0:
|
||||
raise ValueError("unsupported SPKI bit string")
|
||||
rsa_der = bit_string[1:]
|
||||
_, rsa_seq, end = parse_der_tlv(rsa_der, 0, 0x30)
|
||||
if end != len(rsa_der):
|
||||
raise ValueError("trailing data after RSA public key")
|
||||
_, n_bytes, offset = parse_der_tlv(rsa_seq, 0, 0x02)
|
||||
_, e_bytes, offset = parse_der_tlv(rsa_seq, offset, 0x02)
|
||||
if offset != len(rsa_seq):
|
||||
raise ValueError("trailing data after RSA integers")
|
||||
return int.from_bytes(n_bytes, "big"), int.from_bytes(e_bytes, "big")
|
||||
|
||||
|
||||
def packet_header(tag: int, length: int) -> bytes:
|
||||
if length < 192:
|
||||
return bytes([0xC0 | tag, length])
|
||||
if length < 8384:
|
||||
length -= 192
|
||||
return bytes([0xC0 | tag, (length >> 8) + 192, length & 0xFF])
|
||||
return bytes([0xC0 | tag, 0xFF]) + struct.pack(">I", length)
|
||||
|
||||
|
||||
def old_packet_header(tag: int, length: int) -> bytes:
|
||||
if length > 0xFFFF:
|
||||
raise ValueError("old-format two-octet packet length exceeded")
|
||||
return bytes([(tag << 2) | 1]) + struct.pack(">H", length)
|
||||
|
||||
|
||||
def mpi(value: int) -> bytes:
|
||||
if value < 0:
|
||||
raise ValueError("MPI value must be non-negative")
|
||||
raw = value.to_bytes(max(1, (value.bit_length() + 7) // 8), "big")
|
||||
return struct.pack(">H", value.bit_length()) + raw.lstrip(b"\x00")
|
||||
|
||||
|
||||
def subpacket(kind: int, body: bytes) -> bytes:
|
||||
payload = bytes([kind]) + body
|
||||
length = len(payload)
|
||||
if length < 192:
|
||||
return bytes([length]) + payload
|
||||
if length < 8384:
|
||||
length -= 192
|
||||
return bytes([(length >> 8) + 192, length & 0xFF]) + payload
|
||||
return b"\xff" + struct.pack(">I", length) + payload
|
||||
|
||||
|
||||
def public_key_body(n: int, e: int, created_at: int) -> bytes:
|
||||
return (
|
||||
b"\x04"
|
||||
+ struct.pack(">I", created_at)
|
||||
+ bytes([RSA_PUBLIC_KEY_ALGO])
|
||||
+ mpi(n)
|
||||
+ mpi(e)
|
||||
)
|
||||
|
||||
|
||||
def fingerprint(public_body: bytes) -> bytes:
|
||||
return hashlib.sha1(old_packet_header(6, len(public_body)) + public_body).digest()
|
||||
|
||||
|
||||
def armor(kind: str, body: bytes) -> str:
|
||||
b64 = base64.b64encode(body).decode("ascii")
|
||||
crc = crc24(body)
|
||||
checksum = base64.b64encode(crc.to_bytes(3, "big")).decode("ascii")
|
||||
lines = [f"-----BEGIN PGP {kind}-----", ""]
|
||||
lines.extend(b64[i : i + 64] for i in range(0, len(b64), 64))
|
||||
lines.append(f"={checksum}")
|
||||
lines.append(f"-----END PGP {kind}-----")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def crc24(data: bytes) -> int:
|
||||
crc = 0xB704CE
|
||||
for octet in data:
|
||||
crc ^= octet << 16
|
||||
for _ in range(8):
|
||||
crc <<= 1
|
||||
if crc & 0x1000000:
|
||||
crc ^= 0x1864CFB
|
||||
return crc & 0xFFFFFF
|
||||
|
||||
|
||||
def kms_sign(payload: bytes, args: argparse.Namespace) -> bytes:
|
||||
with tempfile.TemporaryDirectory(prefix="burrow-kms-openpgp.") as tmp:
|
||||
input_path = Path(tmp) / "payload"
|
||||
signature_path = Path(tmp) / "signature.b64"
|
||||
input_path.write_bytes(payload)
|
||||
command = [
|
||||
"gcloud",
|
||||
"kms",
|
||||
"asymmetric-sign",
|
||||
"--location",
|
||||
args.kms_location,
|
||||
"--keyring",
|
||||
args.kms_keyring,
|
||||
"--key",
|
||||
args.kms_key,
|
||||
"--version",
|
||||
args.kms_version,
|
||||
"--digest-algorithm",
|
||||
"sha256",
|
||||
"--input-file",
|
||||
str(input_path),
|
||||
"--signature-file",
|
||||
str(signature_path),
|
||||
]
|
||||
if args.gcloud_project:
|
||||
command.extend(["--project", args.gcloud_project])
|
||||
subprocess.run(command, check=True)
|
||||
return base64.b64decode(signature_path.read_text(encoding="utf-8").strip())
|
||||
|
||||
|
||||
def signature_packet(
|
||||
signed_payload: bytes,
|
||||
public_body: bytes,
|
||||
sig_type: int,
|
||||
created_at: int,
|
||||
args: argparse.Namespace,
|
||||
) -> bytes:
|
||||
fpr = fingerprint(public_body)
|
||||
key_id = fpr[-8:]
|
||||
hashed = b"".join(
|
||||
[
|
||||
subpacket(2, struct.pack(">I", created_at)),
|
||||
subpacket(33, b"\x04" + fpr),
|
||||
]
|
||||
)
|
||||
unhashed = subpacket(16, key_id)
|
||||
sig_data = (
|
||||
b"\x04"
|
||||
+ bytes([sig_type, RSA_PUBLIC_KEY_ALGO, SHA256_HASH_ALGO])
|
||||
+ struct.pack(">H", len(hashed))
|
||||
+ hashed
|
||||
)
|
||||
trailer = b"\x04\xff" + struct.pack(">I", len(sig_data))
|
||||
to_hash = signed_payload + sig_data + trailer
|
||||
digest = hashlib.sha256(to_hash).digest()
|
||||
signature = kms_sign(to_hash, args)
|
||||
body = (
|
||||
sig_data
|
||||
+ struct.pack(">H", len(unhashed))
|
||||
+ unhashed
|
||||
+ digest[:2]
|
||||
+ mpi(int.from_bytes(signature, "big"))
|
||||
)
|
||||
return packet_header(2, len(body)) + body
|
||||
|
||||
|
||||
def public_key_command(args: argparse.Namespace) -> int:
|
||||
created_at = args.created_at or int(time.time())
|
||||
n, e = parse_rsa_public_key_from_pem(Path(args.public_key_pem))
|
||||
public_body = public_key_body(n, e, created_at)
|
||||
public_packet = packet_header(6, len(public_body)) + public_body
|
||||
uid = args.user_id.encode("utf-8")
|
||||
uid_packet = packet_header(13, len(uid)) + uid
|
||||
uid_signed_payload = (
|
||||
old_packet_header(6, len(public_body))
|
||||
+ public_body
|
||||
+ b"\xb4"
|
||||
+ struct.pack(">I", len(uid))
|
||||
+ uid
|
||||
)
|
||||
cert_packet = signature_packet(
|
||||
uid_signed_payload,
|
||||
public_body,
|
||||
sig_type=0x13,
|
||||
created_at=created_at,
|
||||
args=args,
|
||||
)
|
||||
key = public_packet + uid_packet + cert_packet
|
||||
output = Path(args.output)
|
||||
if args.armor:
|
||||
output.write_text(armor("PUBLIC KEY BLOCK", key), encoding="utf-8")
|
||||
else:
|
||||
output.write_bytes(key)
|
||||
return 0
|
||||
|
||||
|
||||
def detach_sign_command(args: argparse.Namespace) -> int:
|
||||
created_at = args.created_at or int(time.time())
|
||||
n, e = parse_rsa_public_key_from_pem(Path(args.public_key_pem))
|
||||
public_body = public_key_body(n, e, args.key_created_at)
|
||||
payload = Path(args.input).read_bytes()
|
||||
sig_type = 0x01 if args.text else 0x00
|
||||
packet = signature_packet(payload, public_body, sig_type, created_at, args)
|
||||
output = Path(args.output)
|
||||
if args.armor:
|
||||
output.write_text(armor("SIGNATURE", packet), encoding="utf-8")
|
||||
else:
|
||||
output.write_bytes(packet)
|
||||
return 0
|
||||
|
||||
|
||||
def add_kms_args(parser: argparse.ArgumentParser) -> None:
|
||||
parser.add_argument("--kms-location", default=os.environ.get("BURROW_KMS_LOCATION", "global"))
|
||||
parser.add_argument("--kms-keyring", default=os.environ.get("BURROW_KMS_KEYRING", "burrow-identity"))
|
||||
parser.add_argument("--kms-key", required=not os.environ.get("BURROW_KMS_KEY"), default=os.environ.get("BURROW_KMS_KEY"))
|
||||
parser.add_argument("--kms-version", default=os.environ.get("BURROW_KMS_VERSION", "1"))
|
||||
parser.add_argument("--gcloud-project", default=os.environ.get("GOOGLE_CLOUD_PROJECT"))
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
subcommands = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
public_key = subcommands.add_parser("public-key")
|
||||
public_key.add_argument("--public-key-pem", required=True)
|
||||
public_key.add_argument("--user-id", default="Burrow Package Repository <packages@burrow.net>")
|
||||
public_key.add_argument("--output", required=True)
|
||||
public_key.add_argument("--armor", action="store_true")
|
||||
public_key.add_argument("--created-at", type=int)
|
||||
add_kms_args(public_key)
|
||||
public_key.set_defaults(func=public_key_command)
|
||||
|
||||
detach = subcommands.add_parser("detach-sign")
|
||||
detach.add_argument("--public-key-pem", required=True)
|
||||
detach.add_argument("--key-created-at", type=int, default=1704067200)
|
||||
detach.add_argument("--input", required=True)
|
||||
detach.add_argument("--output", required=True)
|
||||
detach.add_argument("--armor", action="store_true")
|
||||
detach.add_argument("--text", action="store_true")
|
||||
detach.add_argument("--created-at", type=int)
|
||||
add_kms_args(detach)
|
||||
detach.set_defaults(func=detach_sign_command)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"gcloud signing failed: {exc}", file=sys.stderr)
|
||||
return exc.returncode or 1
|
||||
except Exception as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue