1
0
mirror of https://github.com/home-assistant/operating-system.git synced 2026-07-07 06:55:04 +01:00

Add CI check for sufficient validity of the OTA signing certificate (#4751)

* Add CI check for sufficient validity of the OTA signing certificate

To ensure that we don't miss expiration of the signing certificate, add
a CI check that checks its validity and validity of the full chain of
trust, ensuring CA doesn't expire either. Currently set to 25 months,
which should be enough to allow migration of existing devices to new CA
and allow some wiggle room for upgrades and downgrades.

Refs #4743

* Find real issuer when walking the chain

* Add number of certs checked to the suceess message
This commit is contained in:
Jan Čermák
2026-06-08 17:31:55 +02:00
committed by GitHub
parent b721d6df27
commit b7bb667e35
2 changed files with 72 additions and 0 deletions
+10
View File
@@ -206,6 +206,16 @@ jobs:
buildroot-external/scripts/generate-signing-key.sh cert.pem key.pem
echo "self_signed_cert=true" >> $GITHUB_OUTPUT
- name: Check signing certificate validity
env:
RAUC_CERTIFICATE: ${{ secrets.RAUC_CERTIFICATE }}
if: env.RAUC_CERTIFICATE != ''
run: |
echo -e "-----BEGIN CERTIFICATE-----\n${RAUC_CERTIFICATE}\n-----END CERTIFICATE-----" > signing-cert.pem
# 760 days ~ 25 months (the OS support window, with margin)
scripts/check-signing-validity.sh signing-cert.pem buildroot-external/ota/rel-ca.pem 760
rm -f signing-cert.pem
- name: Create signing key
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: steps.generate_signing_key.outcome == 'success'
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Fail the build if the signing certificate, or any CA certificate in its chain,
# expires within MIN_DAYS. Bundles signed now must stay verifiable across the
# whole support window, so we rotate proactively rather than ship a bundle that
# stops validating before the window ends. Run in CI before building.
#
# Usage: check-signing-validity.sh <signing-cert.pem> <keyring.pem> [min_days]
set -euo pipefail
CERT="${1:?usage: $0 <signing-cert.pem> <keyring.pem> [min_days]}"
KEYRING="${2:?usage: $0 <signing-cert.pem> <keyring.pem> [min_days]}"
MIN_DAYS="${3:-760}"
THRESHOLD=$(date -u -d "+${MIN_DAYS} days" +%s)
THRESHOLD_DATE=$(date -u -d "+${MIN_DAYS} days" +%Y-%m-%d)
fail() { echo "::error::$*"; exit 1; }
enddate() { openssl x509 -in "$1" -noout -enddate | cut -d= -f2; }
subject() { openssl x509 -in "$1" -noout -subject | sed 's/^subject=//'; }
too_soon() { [ "$(date -u -d "$(enddate "$1")" +%s)" -lt "$THRESHOLD" ]; }
# 1) CA chain (most urgent: a CA rotation is a full migration). Walk the chain
# leaf -> intermediate -> root through the keyring, checking each CA cert. Only
# the chain the leaf actually uses is walked, so the deliberately-expiring old CA
# certs (kept in the keyring for downgrades) are ignored.
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
awk -v d="$tmp" '/-----BEGIN CERTIFICATE-----/{n++} {print > (d"/"n".pem")}' "$KEYRING"
# Issuer of $1 within the keyring, picked by signature (not just subject name) so
# same-subject certs - e.g. a future same-CN key rotation - are disambiguated.
find_issuer() {
local cand ih
ih=$(openssl x509 -in "$1" -noout -issuer_hash)
for cand in "$tmp"/*.pem; do
# Narrow by subject name, then confirm by signature (-partial_chain treats
# the candidate as a standalone anchor, -no_check_time leaves expiry to us).
# Only the real signer verifies, so same-subject siblings are disambiguated.
[ "$(openssl x509 -in "$cand" -noout -subject_hash)" = "$ih" ] || continue
openssl verify -partial_chain -no_check_time -CAfile "$cand" "$1" >/dev/null 2>&1 \
&& { printf '%s\n' "$cand"; return 0; }
done
return 1
}
cur="$CERT"; hops=0
while :; do
[ "$(openssl x509 -in "$cur" -noout -subject_hash)" = \
"$(openssl x509 -in "$cur" -noout -issuer_hash)" ] && break # self-signed root
next=$(find_issuer "$cur") || fail "Issuer of '$(subject "$cur")' not found in ${KEYRING} - the signing certificate does not chain to this keyring."
if too_soon "$next"; then
fail "CA certificate '$(subject "$next")' expires $(enddate "$next") (within ${MIN_DAYS} days, before ${THRESHOLD_DATE}). A new CA should be generated, the keyrings (buildroot-external/ota/*-ca.pem) updated, and the cross-signed intermediate added to the bundle."
fi
cur="$next"; hops=$((hops + 1))
[ "$hops" -le 10 ] || fail "certificate chain too long (loop?)"
done
# 2) signing leaf
if too_soon "$CERT"; then
fail "Signing certificate expires $(enddate "$CERT") (within ${MIN_DAYS} days, before ${THRESHOLD_DATE}). A new leaf certificate needs to be generated and the CI signing key updated."
fi
echo "Signing certificate and CA chain (in total $((hops + 1)) certificates) are valid for at least ${MIN_DAYS} days (past ${THRESHOLD_DATE})."