Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions .github/workflows/distro.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ on:
- '*.md'
- '.github/workflows/ci.yml'
workflow_dispatch:
inputs:
role:
description: "Build and test the release as a development build, or as a production one signed with a throwaway key medium: the release flow, rehearsed"
type: choice
options: [development, production]
default: development

# Said in the reviewed file rather than left to a repository setting. The cache
# and the artifacts use the runner's own credential, not this token.
Expand Down Expand Up @@ -334,19 +340,41 @@ jobs:
# images-production (tools/production-pair.sh). Then release A, and B =
# A's version with .1 (sorts after A), which the update test applies over
# A: the suites take the release built last as the one under test. A
# tag's run builds the tag's version as B, over a 0.0.0 A.
# tag's run builds the tag's version as B, over a 0.0.0 A. A production
# version (v1 and up, or the role asked for) is built as a release is,
# signed with a throwaway key medium that lives only while the media are
# built: the release flow rehearsed, and no job publishes its export.
- name: Stage 06, the media (production 1.0.0 and 1.0.1, release A and the update B)
timeout-minutes: 90
run: |
make production-pair KRYPTIK_KRYPTIKD_BIN=/mnt/kryptik/kryptikd-musl KRYPTIK_WLPROXY_BIN=/mnt/kryptik/kryptik-wlproxy-musl
ls -la "$KRYPTIK_WORK/images-production"
ROLE=development
if [ "${{ inputs.role }}" = production ]; then ROLE=production; fi
if [ "$GITHUB_REF_TYPE" = tag ]; then
VER_A=0.0.0; VER_B="${GITHUB_REF_NAME#v}"
case "$GITHUB_REF_NAME" in v0.*) ;; *) ROLE=production ;; esac
elif [ "$ROLE" = production ]; then
VER_A=0.0.0; VER_B=0.0.1
else
VER_A="0.1.$(date -u +%Y%m%d).${GITHUB_SHA::8}"; VER_B="${VER_A}.1"
fi
make media KRYPTIK_VERSION="$VER_A" KRYPTIK_KRYPTIKD_BIN=/mnt/kryptik/kryptikd-musl KRYPTIK_WLPROXY_BIN=/mnt/kryptik/kryptik-wlproxy-musl
make media KRYPTIK_VERSION="$VER_B" KRYPTIK_KRYPTIKD_BIN=/mnt/kryptik/kryptikd-musl KRYPTIK_WLPROXY_BIN=/mnt/kryptik/kryptik-wlproxy-musl
echo "a ${ROLE} release: ${VER_B}, updating from ${VER_A}"
KEYS=()
if [ "$ROLE" = production ]; then
tools/throwaway-key-medium.sh /mnt/kryptik/medium
KEYS=(KRYPTIK_ROLE=production KRYPTIK_KEYS=/mnt/kryptik/medium)
fi
make media KRYPTIK_VERSION="$VER_A" "${KEYS[@]}" KRYPTIK_KRYPTIKD_BIN=/mnt/kryptik/kryptikd-musl KRYPTIK_WLPROXY_BIN=/mnt/kryptik/kryptik-wlproxy-musl
make media KRYPTIK_VERSION="$VER_B" "${KEYS[@]}" KRYPTIK_KRYPTIKD_BIN=/mnt/kryptik/kryptikd-musl KRYPTIK_WLPROXY_BIN=/mnt/kryptik/kryptik-wlproxy-musl
if [ "$ROLE" = production ]; then
# The suites sign their control disks with the medium's own key,
# from a copy, as a release's acceptance does; the rest of the
# medium is gone before anything is packed.
sudo mkdir -p "$KRYPTIK_WORK/keys/medium"
sudo cp /mnt/kryptik/medium/kryptik-testctl /mnt/kryptik/medium/kryptik-testctl.pub "$KRYPTIK_WORK/keys/medium/"
rm -rf /mnt/kryptik/medium
fi
ls -la "$KRYPTIK_WORK/images" | grep -E 'usb.img|\.iso|payload'

# Never the private halves of the keys: the acceptance needs the
Expand Down Expand Up @@ -443,7 +471,9 @@ jobs:
SUITES: ${{ matrix.suites }}
run: |
rc=0
sudo env KRYPTIK_WORK="$KRYPTIK_WORK" KRYPTIK_SOURCES="$KRYPTIK_SOURCES" NO_COLOR=1 \
# A release's media honour control disks signed by their medium's key: its copy, when there is one.
k="$KRYPTIK_WORK/keys/medium/kryptik-testctl"; [ -f "$k" ] || k=""
sudo env KRYPTIK_WORK="$KRYPTIK_WORK" KRYPTIK_SOURCES="$KRYPTIK_SOURCES" NO_COLOR=1 KRYPTIK_TESTCTL_KEY="$k" \
TMPDIR="$TMPDIR" CARGO_TARGET_DIR="$CARGO_TARGET_DIR" tools/acceptance.sh --only "$SUITES" || rc=$?
[ "$rc" -eq 0 ] || [ "$rc" -eq 2 ]

Expand Down Expand Up @@ -537,13 +567,16 @@ jobs:
echo "make acceptance did not pass; see the acceptance-report artifact"
exit 1

# A tag's run ends on the Releases page: the tested export, the
# corresponding source and the acceptance logs, as a draft to publish after
# a look (docs/releases.md). The one job that writes to the repository.
# A development release's tag (v0.x) ends on the Releases page: the tested
# export, the corresponding source and the acceptance logs, as a draft to
# publish after a look (docs/releases.md). A production release is built by
# hand with the key medium and published with the same tool; its tag's run
# rehearsed that with a throwaway medium, and nothing signed by one goes on
# the page. The one job that writes to the repository.
release:
name: Draft the release
needs: [verdict]
if: ${{ !cancelled() && github.ref_type == 'tag' && needs.verdict.result == 'success' }}
if: ${{ !cancelled() && github.ref_type == 'tag' && startsWith(github.ref_name, 'v0.') && needs.verdict.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 120
permissions:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ CHROOT_ENV := KRYPTIK_ROOT="$(ROOT)" \
TERM="$(TERM)" \
NO_COLOR="$(NO_COLOR)" \
$(if $(CARGO_TARGET_DIR),CARGO_TARGET_DIR="$(CARGO_TARGET_DIR)") \
$(if $(KRYPTIK_TESTCTL_KEY),KRYPTIK_TESTCTL_KEY="$(KRYPTIK_TESTCTL_KEY)") \
$(if $(TMPDIR),TMPDIR="$(TMPDIR)")

CHROOT_RUN := $(SUDO) env $(CHROOT_ENV) "$(CHROOTD)"
Expand Down
48 changes: 32 additions & 16 deletions build/lib/release-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,20 @@ release_version() { # release_version ROLE VERSION
dev_keys() {
local rel="${KRYPTIK_WORK}/keys/release" sb="${KRYPTIK_WORK}/keys/sb" k
mkdir -p "$rel" "$sb"; chmod 0700 "$rel" "$sb"
for k in kryptik-release kryptik-latest; do
for k in kryptik-release kryptik-latest kryptik-testctl; do
if [[ ! -f "${rel}/${k}" ]]; then
ssh-keygen -q -t ed25519 -N "" -C "${k} (developer)" -f "${rel}/${k}" < /dev/null
echo "generated a new developer key: ${k}"
fi
done
# Each key honoured in its own namespaces only: the release key's for
# manifests and the media's checksums, and the statement key's, kept at hand
# for re-signing the channel's statement, for that alone.
# manifests and the media's checksums; the statement key's, kept at hand
# for re-signing the channel's statement, for that alone; and the
# control-disk key's, which arms an unattended install and nothing else.
{
printf 'kryptik-release namespaces="kryptik-release,kryptik-media" %s\n' "$(cut -d' ' -f1,2 "${rel}/kryptik-release.pub")"
printf 'kryptik-latest namespaces="kryptik-latest" %s\n' "$(cut -d' ' -f1,2 "${rel}/kryptik-latest.pub")"
printf 'kryptik-testctl namespaces="kryptik-testctl" %s\n' "$(cut -d' ' -f1,2 "${rel}/kryptik-testctl.pub")"
} > "${rel}/release-signers"
if [[ ! -f "${sb}/kryptik-sb.key" ]]; then
openssl req -new -x509 -newkey rsa:3072 -nodes -days 3650 -sha256 \
Expand All @@ -69,20 +71,25 @@ EOF

# A probe signed by each developer key and by a foreign one, in each namespace,
# verified through the anchor as its readers verify: only the release key, in
# kryptik-release and kryptik-media, and the statement key, in kryptik-latest,
# may pass. The production keys sign nothing but what they are for, so this
# runs for development alone.
# kryptik-release and kryptik-media, the statement key, in kryptik-latest, and
# the control-disk key, in kryptik-testctl, may pass. The production keys sign
# nothing but what they are for, so this runs for development alone.
probe_anchor() {
local t key ns who want got
local t key ns who want got tkey
tkey="$(dirname "$RELEASE_KEY")/kryptik-testctl"
t="$(mktemp -d)"
ssh-keygen -q -t ed25519 -N "" -f "$t/foreign" < /dev/null
printf 'probe\n' > "$t/probe"
for key in "$RELEASE_KEY" "$LATEST_KEY" "$t/foreign"; do
for ns in kryptik-release kryptik-media kryptik-latest; do
who=kryptik-release; [[ "$ns" == kryptik-latest ]] && who=kryptik-latest
for key in "$RELEASE_KEY" "$LATEST_KEY" "$tkey" "$t/foreign"; do
for ns in kryptik-release kryptik-media kryptik-latest kryptik-testctl; do
case "$ns" in
kryptik-latest) who=kryptik-latest ;;
kryptik-testctl) who=kryptik-testctl ;;
*) who=kryptik-release ;;
esac
want=refused
case "$key:$ns" in
"$RELEASE_KEY:kryptik-release"|"$RELEASE_KEY:kryptik-media"|"$LATEST_KEY:kryptik-latest") want=accepted ;;
"$RELEASE_KEY:kryptik-release"|"$RELEASE_KEY:kryptik-media"|"$LATEST_KEY:kryptik-latest"|"$tkey:kryptik-testctl") want=accepted ;;
esac
rm -f "$t/probe.sig"
# A refusal counts only when there was a signature to refuse.
Expand Down Expand Up @@ -115,17 +122,23 @@ medium_keys() {
if [[ -e "${m}/kryptik-latest" && ! -f "${m}/kryptik-latest.pub" ]]; then
die "the key medium ${m} has kryptik-latest but no kryptik-latest.pub"
fi
for f in release-signers kryptik-release kryptik-release.pub kryptik-latest kryptik-latest.pub kryptik-sb.key kryptik-sb.crt; do
if [[ -e "${m}/kryptik-testctl" && ! -f "${m}/kryptik-testctl.pub" ]]; then
die "the key medium ${m} has kryptik-testctl but no kryptik-testctl.pub"
fi
for f in release-signers kryptik-release kryptik-release.pub kryptik-latest kryptik-latest.pub kryptik-testctl kryptik-testctl.pub kryptik-sb.key kryptik-sb.crt; do
[[ ! -L "${m}/${f}" ]] || die "${m}/${f} is a symlink: the medium holds its files itself"
done
for f in kryptik-release kryptik-latest kryptik-sb.key; do
for f in kryptik-release kryptik-latest kryptik-testctl kryptik-sb.key; do
if [[ -e "${m}/${f}" ]]; then private_ok "${m}/${f}"; fi
done
anchor_ok "${m}/release-signers"
pub_in_anchor "${m}/kryptik-release.pub" kryptik-release "${m}/release-signers"
if [[ -e "${m}/kryptik-latest" ]]; then
pub_in_anchor "${m}/kryptik-latest.pub" kryptik-latest "${m}/release-signers"
fi
if [[ -e "${m}/kryptik-testctl" ]]; then
pub_in_anchor "${m}/kryptik-testctl.pub" kryptik-testctl "${m}/release-signers"
fi
openssl x509 -in "${m}/kryptik-sb.crt" -noout -checkend 0 > /dev/null 2>&1 \
|| die "${m}/kryptik-sb.crt is not a certificate in force"
ANCHOR="${m}/release-signers"; RELEASE_KEY="${m}/kryptik-release"; LATEST_KEY=""
Expand All @@ -144,8 +157,10 @@ private_ok() {
}

# kryptik-release, held to kryptik-release (manifests) and kryptik-media (the
# media's checksums), and kryptik-latest, held to kryptik-latest alone, with
# Ed25519 keys (a security key's included). A principal may be listed more
# media's checksums), kryptik-latest, held to kryptik-latest alone, and
# kryptik-testctl, if listed, held to kryptik-testctl alone (the control disk
# that arms an unattended install), with Ed25519 keys (a security key's
# included). A principal may be listed more
# than once, as the old key and its replacement are while a key is changed,
# but a key only once: one that could sign both releases and statements would
# undo the split.
Expand All @@ -156,7 +171,8 @@ anchor_ok() {
case "$p" in
kryptik-release) want='namespaces="kryptik-release,kryptik-media"' ;;
kryptik-latest) want='namespaces="kryptik-latest"' ;;
*) die "${a}: ${p} is neither kryptik-release nor kryptik-latest" ;;
kryptik-testctl) want='namespaces="kryptik-testctl"' ;;
*) die "${a}: ${p} is neither kryptik-release, kryptik-latest nor kryptik-testctl" ;;
esac
[[ "$ns" == "$want" ]] || die "${a}: ${p} is not held to its own namespaces (${want})"
case "$t" in ssh-ed25519|sk-ssh-ed25519@openssh.com) ;; *) die "${a}: ${p}'s key is ${t}, not Ed25519" ;; esac
Expand Down
18 changes: 17 additions & 1 deletion build/service-scripts/testctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Test control for install media, sourced by boot-time services. A disk
# labelled kryptik-testctl carries a key=value file, read only when booted from
# an install medium (kryptik.media=), so such a disk cannot reinstall or shut
# down an installed system.
# down an installed system, and only when the kryptik-testctl key the medium's
# anchor lists signed it, so no one else's disk can arm an install either.
# testctl_load 0, with TESTCTL_FILE set, when a control file was read
# testctl_get KEY the value, or empty
# Keys: install_target=/dev/vdb smoke_poweroff=1 preseed_user=NAME
Expand All @@ -11,6 +12,16 @@

TESTCTL_MNT=/run/kryptik/testctl
TESTCTL_FILE=""
TESTCTL_ANCHOR="${TESTCTL_ANCHOR:-/usr/share/kryptik/trust/release-signers}"

# The file and a signature over it by the kryptik-testctl key the anchor
# lists, in that key's own namespace: the holder of that key alone can arm an
# install on a machine that boots this medium.
testctl_signed() { # testctl_signed FILE
[ -r "$1" ] && [ -r "$1.sig" ] || return 1
ssh-keygen -Y verify -f "$TESTCTL_ANCHOR" -I kryptik-testctl -n kryptik-testctl \
-s "$1.sig" < "$1" > /dev/null 2>&1
}

testctl_media() {
grep -qs '^media=.\+' /run/kryptik/boot-identity 2>/dev/null && return 0
Expand All @@ -27,6 +38,11 @@ testctl_load() {
mount -o ro,nosuid,nodev,noexec "$dev" "$TESTCTL_MNT" 2>/dev/null || return 1
fi
[ -r "$TESTCTL_MNT/kryptik-test.conf" ] || return 1
if ! testctl_signed "$TESTCTL_MNT/kryptik-test.conf"; then
echo "testctl: the control file on ${dev} is not signed by this medium's kryptik-testctl key; ignored"
umount "$TESTCTL_MNT" 2>/dev/null
return 1
fi
TESTCTL_FILE="$TESTCTL_MNT/kryptik-test.conf"
echo "testctl: control file read from ${dev} (install medium only)"
return 0
Expand Down
14 changes: 13 additions & 1 deletion docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ make media KRYPTIK_ROLE=production KRYPTIK_KEYS=/media/<medium> KRYPTIK_VERSION=

The medium holds `release-signers` (the anchor the image will trust),
`kryptik-release` and `kryptik-release.pub`, `kryptik-sb.key` and
`kryptik-sb.crt`, and optionally `kryptik-latest` and `kryptik-latest.pub`.
`kryptik-sb.crt`, and optionally `kryptik-latest` and `kryptik-latest.pub`
and `kryptik-testctl` and `kryptik-testctl.pub`, which the build reads only
to check they are the anchor's.
Its private keys must be readable by their owner alone, who is root or the
user running the build, and it must not be inside the work or output tree.
Stage 06 checks all of that before it signs anything. It makes no key and
Expand Down Expand Up @@ -122,3 +124,13 @@ make acceptance EXPORT=/tmp/kryptik-export

`tools/release-publish.sh /tmp/kryptik-export` puts that export on the
repository's Releases page once tag `v0.1.2` is pushed ([releases](releases.md)).

The suites arm their unattended installs with a control disk the medium
under test checks against its anchor. A development medium honours the
build's own `kryptik-testctl` (under `<work>/keys/release`), the default; a
production release's media honour the ceremony's, so its acceptance names a
copy: `make acceptance EXPORT=DIR KRYPTIK_TESTCTL_KEY=<copy>/kryptik-testctl`
([release keys](release-keys.md)). The boot tests enrol the Secure Boot
certificate the medium under test carries, and the export publishes that
certificate: a production release is tested as a machine that enrolled its
certificate boots it.
Loading
Loading