Skip to main content
Version: Omada Identity Cloud Private

Release package signing

Every Omada Identity Cloud Private release package is signed with a code-signing certificate issued to Omada A/S by a public certificate authority. Signing lets you confirm, before installing, that the package you downloaded actually came from Omada and has not been altered in transit.

What ships with a signed release

Each release download includes two signed ZIP packages (the main release package, oicp_<version>.zip, and the install package, oicp_install_<version>.zip) together with a set of verification files.

Both ZIP packages in a release share:

  • The same certificate (omada-codesign.pem).
  • Chain (omada-codesign-chain.pem).
  • Verification scripts, one per platform (both also provided verbatim in the Scripts section below):
    • verify-oicp.sh for Linux.
    • verify-oicp.ps1 for Windows.
  • Background and context on the verification process (VERIFY.md).

Each ZIP has its own files:

  • Checksum, a SHA-256 hash of the ZIP used to confirm the download was not corrupted or truncated in transit (.sha256).
  • Signature (.sig.json).
  • Timestamp (.tsr).

How trust is established

The root of trust comes from your own operating system's trust store, not from anything Omada ships alongside the package. Therefore, the release download itself never includes a root certificate.

This is what makes the verification reliable: the certificate chains to a root your machine already trusts (for example, DigiCert Trusted Root G4, present in essentially every OS trust store and Linux ca-certificates bundle), instead of relying on any supplied root certificate that could be replaced or revoked.

important

Omada's signing certificate is renewed periodically, and its exact subject spelling or issuing CA may change between releases as a result. That is expected and does not weaken verification: trust is always anchored to your own OS trust store, never to any specific detail of the certificate Omada ships.

Verifying a release

To verify a release, run the bundled script for your platform. Both scripts perform the same five checks, in the same order, and reach the same verdict: they are the easiest way to confirm that the ZIP is authentic and unmodified:

  1. Timestamp – proves when the package was signed, verified against an independent timestamp authority.
  2. Chain validation – proves provenance: the signing certificate chains to a root your own machine already trusts. This is the step that establishes the release came from Omada.
  3. Signer identity – proves who: that the validated certificate was issued to Omada specifically, and that it matches the SHA-256 thumbprint Omada publishes for this release – a required input, not an optional extra; see Published thumbprint below.
  4. Signature – proves the ZIP matches what was signed, using the public key from the now-validated certificate.
  5. Checksum – catches a corrupted or truncated download.

If chain validation fails, check whether your operating system's CA bundle is out of date (for example, apt install ca-certificates) before assuming the release itself is at fault.

note

Both scripts also ask the certificate authority whether the certificate has been revoked, which needs outbound HTTPS access.

  • Confirmed revoked – the run stops. Do not install the package, and contact Omada support for guidance.
  • Revocation status cannot be established (for example, an unreachable responder – common on an air-gapped machine or behind a restrictive proxy) – reported as a warning, and the rest of the verification still completes. If you need the revocation status established before installing, ask Omada support to confirm the certificate's current status.

Published thumbprint

Both scripts require one further check beyond the ZIP itself: that the signing certificate – not just the ZIP – is the exact one Omada issued for this release. Without it, the script stops before doing any work. This is separate from the .sha256 checksum described above, which only protects against a corrupted or truncated download.

A thumbprint is a hash of the entire certificate, so it identifies one exact certificate and nothing else. Omada publishes it here, on the documentation portal – a channel separate from the release download itself – so the value cannot travel with, or be replaced by, a tampered package. Every other file in the release bundle is the download's own to construct; this value is not, which is why the scripts insist on it.

important

Each new release adds a row to the table below. Rows are never removed or overwritten: a disappearing entry would make that release unverifiable.

In use sinceSHA-256 thumbprint
August 2026C95E2328EB73FF105C96F3436351A5F2C23B519ADEE70E82FA0A73AA2EED92A7

Supply the value for the release you're verifying:

Tool: Bash shell

EXPECTED_THUMBPRINT=<thumbprint from the table above> ./verify-oicp.sh oicp_<version>.zip

Tool: PowerShell

pwsh -File verify-oicp.ps1 oicp_<version>.zip -ExpectedThumbprint '<thumbprint from the table above>'
tip
  • Colons, spaces, and capitalization are ignored, so paste the value in whatever form you copied it.
  • Windows' certificate dialog shows the certificate's SHA-1 thumbprint (40 hex digits), not the SHA-256 thumbprint (64 hex digits) published here. The scripts expect the SHA-256 value and will tell you if you supply the wrong one, rather than treating it as a bad release.

Scripts

The full source of both scripts is below, exactly as shipped in the release package. They are provided here for transparency.

Requires openssl, sha256sum, python3, date, and mktemp – all standard on Linux.

EXPECTED_THUMBPRINT=<thumbprint from the table above> ./verify-oicp.sh oicp_<version>.zip
Show full script (verify-oicp.sh)
#!/usr/bin/env bash
# verify-oicp.sh — Verify an OICP release package against Omada's signing certificate.
# Usage — EXPECTED_THUMBPRINT is required. It is published on the Omada documentation portal, and
# is the one value here that does not travel with the download:
# EXPECTED_THUMBPRINT=<value> bash verify-oicp.sh oicp_14.1.0.zip
# EXPECTED_THUMBPRINT=<value> ./verify-oicp.sh /downloads/oicp_14.1.0.zip # after chmod +x
#
# Any path works: the files below are read from beside the ZIP, not from the current directory.
#
# Expects these files alongside the ZIP (all shipped with every release):
# oicp_14.1.0.zip.sha256 sha256sum-compatible checksum
# oicp_14.1.0.zip.sig.json signing response: {algorithm, keyId, signature}
# oicp_14.1.0.zip.tsr RFC 3161 timestamp response
# omada-codesign.pem Omada's signing certificate (leaf)
# omada-codesign-chain.pem the issuing intermediate(s)
#
# The release does NOT ship a root certificate, and it must not: the root comes from your own
# operating system's trust store. That is what makes this verification meaningful. Whoever
# hands you the download cannot also hand you the thing you check it against.
#
# Needs outbound HTTP to the certificate authority, to ask whether the certificate has been
# withdrawn. A responder it cannot reach is a warning and the other checks continue; only a clear
# "revoked" stops the verification.
#
# Environment overrides:
# EXPECTED_THUMBPRINT REQUIRED. SHA-256 thumbprint of Omada's signing certificate, as published
# on the Omada documentation portal alongside the release you are verifying.
# The only anchor here that did not travel with the download, which is why
# it is not optional. Each release has its own value.
# CA_BUNDLE path to a PEM CA bundle (default: your platform's system trust store)
# NO_COLOR set to disable colour (https://no-color.org/)
set -euo pipefail

# Colour only on an interactive terminal, and respect NO_COLOR (https://no-color.org/).
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
C_GREEN=$'\033[32m'
C_RED=$'\033[31m'
C_YELLOW=$'\033[33m'
C_BOLD=$'\033[1m'
C_RESET=$'\033[0m'
else
C_GREEN=''
C_RED=''
C_YELLOW=''
C_BOLD=''
C_RESET=''
fi

ZIP_ARG="${1:?Usage: $0 <path-to-oicp-release.zip>}"

# Matched as a prefix of the signature's keyId, which also pins the key version - that changes
# every time the certificate is rotated.
EXPECTED_VAULT="https://kvcertkv.vault.azure.net/"

# Every failure exits here, so no later step can run on an unverified premise.
fail() {
echo "${C_RED}$1${C_RESET}" >&2
exit 1
}

warn() {
echo "${C_YELLOW}! $1${C_RESET}" >&2
}

echo "${C_BOLD}Omada — OICP Release Verification${C_RESET}"

echo ""
echo "=== Checking prerequisites ==="
for cmd in sha256sum openssl python3 date mktemp; do
command -v "$cmd" >/dev/null 2>&1 || fail "ERROR: $cmd not found. Install it with your OS package manager (coreutils / openssl / python3)."
done
echo "${C_GREEN}✓ OK${C_RESET}"

# Shape here, value at step 3, so a mistyped thumbprint stops the run before any work. Non-hex
# characters are dropped: colons, spaces and case are how different tools render the same value.
WANT_TP="${EXPECTED_THUMBPRINT:-}"
WANT_TP="${WANT_TP,,}"
WANT_TP="${WANT_TP//[!0-9a-f]/}"

if [[ -z "${EXPECTED_THUMBPRINT:-}" ]]; then
fail "EXPECTED_THUMBPRINT is required.
It is the SHA-256 thumbprint of Omada's signing certificate, published on the Omada
documentation portal alongside the release you are verifying. It is the one value in this
procedure that did not travel with the download, which is what makes it worth checking:

EXPECTED_THUMBPRINT=<value from the portal> ./verify-oicp.sh <release.zip>

Each release has its own value. If you cannot find it, ask Omada support for it rather than
proceeding without it."
fi

# Bad input, not a bad release - reported as such, and the run still stops.
if [[ "${#WANT_TP}" -eq 40 ]]; then
fail "EXPECTED_THUMBPRINT looks like a SHA-1 thumbprint: it has 40 hex digits.
Windows shows SHA-1 in its certificate dialogue and calls it the thumbprint, but Omada
publishes the SHA-256 one, which has 64. Copy that value from the Omada documentation portal
and run this again. Nothing here says anything about the release - this run checked nothing."
elif [[ "${#WANT_TP}" -ne 64 ]]; then
fail "EXPECTED_THUMBPRINT is not a SHA-256 thumbprint.
Ignoring any colons or spaces it has ${#WANT_TP} hex digits, and a SHA-256 thumbprint has 64.
Copy it again from the Omada documentation portal. Nothing here says anything about the
release - this run checked nothing."
fi
echo "${C_GREEN}✓ Thumbprint supplied${C_RESET}"

# After the prerequisite check, so a missing mktemp is reported by it rather than aborting with
# a bare shell error.
SIG_DER="$(mktemp)"
PUBKEY="$(mktemp)"
trap 'rm -f "$SIG_DER" "$PUBKEY"' EXIT

echo ""
echo "=== Checking required files ==="

[[ -f "$ZIP_ARG" ]] || fail "ERROR: required file not found: $ZIP_ARG
Give the path of the release ZIP itself, and re-download the full release bundle if it is
missing - all files must sit in the same directory."

# Resolved relative to the ZIP, not the current directory, so a release verifies from anywhere.
ZIP_DIR="$(cd -- "$(dirname -- "$ZIP_ARG")" && pwd)"
ZIP="$ZIP_DIR/$(basename -- "$ZIP_ARG")"
SIG_JSON="${ZIP}.sig.json"
CHECKSUM="${ZIP}.sha256"
TSR="${ZIP}.tsr"
LEAF="$ZIP_DIR/omada-codesign.pem"
CHAIN="$ZIP_DIR/omada-codesign-chain.pem"

for f in "$SIG_JSON" "$CHECKSUM" "$TSR" "$LEAF" "$CHAIN"; do
if [[ ! -f "$f" ]]; then
case "$f" in
"$TSR") fail "ERROR: required file not found: $f
The timestamp is mandatory. Without it there is no trusted signing time, so the
certificate cannot be validated once it expires. Re-download the full release bundle." ;;
"$CHAIN") fail "ERROR: required file not found: $f
This file contains the intermediate certificate linking Omada's certificate to a
root your operating system already trusts. Without it the chain cannot be built
(Linux will not fetch it for you). Re-download the full release bundle." ;;
*) fail "ERROR: required file not found: $f
Re-download the full release bundle - all files must sit in the same directory." ;;
esac
fi
done
echo "${C_GREEN}✓ OK${C_RESET}"

# The customer's own root store: the trust anchor is not something Omada supplies.
if [[ -z "${CA_BUNDLE:-}" ]]; then
for candidate in \
/etc/ssl/certs/ca-certificates.crt \
/etc/pki/tls/certs/ca-bundle.crt \
/etc/ssl/ca-bundle.pem \
/etc/ssl/cert.pem
do
if [[ -f "$candidate" ]]; then
CA_BUNDLE="$candidate"
break
fi
done
fi
[[ -n "${CA_BUNDLE:-}" && -f "$CA_BUNDLE" ]] || fail "ERROR: could not locate your system CA bundle.
Set CA_BUNDLE=/path/to/ca-certificates.crt and re-run."
echo "Trust store: $CA_BUNDLE"

# ---------------------------------------------------------------------------
# Step 1 — Timestamp. MUST run first: step 2 validates the chain *at* this time.
# Proves: when the signature was made, attested by an independent authority.
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 1/5 — Verifying RFC 3161 timestamp ==="

# Read before the token is verified, because the verification below judges the authority's own
# certificate as of that time rather than as of today. Authority certificates expire too, and a
# release that outlived the one that timestamped it must still verify. verify-oicp.ps1 evaluates
# the same chain at the same instant.
SIGN_TIME="$(openssl ts -reply -in "$TSR" -text 2>/dev/null | sed -n 's/.*Time stamp: *//p')"
[[ -n "$SIGN_TIME" ]] || fail "Could not read the signing time from $TSR.
The file is not a readable RFC 3161 timestamp response. Re-download the full release bundle."

EPOCH="$(date -u -d "$SIGN_TIME" +%s 2>/dev/null || true)"
[[ -n "$EPOCH" ]] || fail "Could not convert the signing time '$SIGN_TIME' to a timestamp."

# -attime on `ts` exists only from OpenSSL 1.1.0, so probe rather than assume: an older
# distribution keeps verifying, with the authority's certificate judged at the current time.
# Captured rather than piped, as with the purpose probe below - pipefail would report openssl's
# own exit status instead of the match.
TS_HELP="$(openssl ts -help 2>&1 || true)"
TS_ATTIME_ARGS=()
case "$TS_HELP" in
*-attime*) TS_ATTIME_ARGS=(-attime "$EPOCH") ;;
esac

openssl ts -verify "${TS_ATTIME_ARGS[@]+"${TS_ATTIME_ARGS[@]}"}" \
-in "$TSR" -data "$SIG_JSON" -CAfile "$CA_BUNDLE" >/dev/null 2>&1 \
|| fail "Timestamp verification FAILED.
The .tsr file does not correspond to this .sig.json, or the timestamp authority's
certificate does not chain to a root in your trust store. Do not install this package."

echo "${C_GREEN}✓ Signed at: $SIGN_TIME${C_RESET}"

# ---------------------------------------------------------------------------
# Step 2 — Chain validation at signing time. THIS IS THE TRUST DECISION.
# Proves: PROVENANCE. The certificate chains to a root your OS already trusts,
# and it was valid when the signature was made.
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 2/5 — Validating certificate chain against your trust store ==="
echo "(this is the step that proves the release came from Omada)"

# Checked explicitly, because "-purpose codesign" is not available on every OpenSSL (see below)
# and dropping the requirement silently on older versions would weaken the check without saying so.
if ! openssl x509 -in "$LEAF" -noout -ext extendedKeyUsage 2>/dev/null | grep -q 'Code Signing'; then
fail "The certificate is not marked for code signing.
Its Extended Key Usage does not include Code Signing, so it is not a certificate that may
be used to sign a release. Do not install this package; contact Omada support."
fi

# "-purpose codesign" arrived after the 3.0 series; Ubuntu 24.04 LTS ships 3.0.13, where passing
# it aborts with "Invalid purpose". Used when available, as a library-enforced check on top of the
# EKU test above. Captured rather than piped: pipefail would report openssl's own non-zero exit
# instead of grep's match, making the probe conclude the opposite of the truth.
PURPOSE_ARGS=()
PURPOSE_PROBE="$(openssl verify -purpose codesign -CAfile "$CA_BUNDLE" "$LEAF" 2>&1 || true)"
case "$PURPOSE_PROBE" in
*"Invalid purpose"*) : ;;
*) PURPOSE_ARGS=(-purpose codesign) ;;
esac

openssl verify -attime "$EPOCH" "${PURPOSE_ARGS[@]+"${PURPOSE_ARGS[@]}"}" \
-CAfile "$CA_BUNDLE" -untrusted "$CHAIN" "$LEAF" \
|| fail "Certificate chain validation FAILED.
Omada's certificate could not be validated against a root in your own trust store, as of
the signing time above. This is the check that establishes the release came from Omada, so
it is the one that has to pass. Do not install this package until it does.
If every other step passed, first confirm your CA bundle is up to date -
e.g. 'apt install ca-certificates' or 'yum update ca-certificates'. If that does not
resolve it, contact Omada support."

echo "${C_GREEN}✓ Chain validates to a root in your trust store${C_RESET}"

# A certificate can be withdrawn by the authority that issued it before it expires, and it carries
# the address where that withdrawal is published.
# -issuer is the certificate that signed the leaf: the first entry in the chain file.
OCSP_URI="$(openssl x509 -in "$LEAF" -noout -ocsp_uri 2>/dev/null | head -1)"
if [[ -z "$OCSP_URI" ]]; then
warn "This certificate names no OCSP responder, so its revocation status could not be checked.
Every other check is unaffected."
else
# Anything other than a clear "revoked" warns rather than failing: an unreachable or unverifiable
# responder says nothing about this release. verify-oicp.ps1 reaches the same verdict from the
# chain status.
OCSP_OUT="$(openssl ocsp -issuer "$CHAIN" -cert "$LEAF" -url "$OCSP_URI" \
-no_nonce -timeout 10 -CAfile "$CA_BUNDLE" 2>&1 || true)"
case "$OCSP_OUT" in
*": revoked"*)
fail "The signing certificate has been REVOKED.
The authority that issued it has withdrawn it. Do not install this package, and contact Omada
support for guidance." ;;
*": good"*)
echo "${C_GREEN}✓ Certificate is not revoked (asked $OCSP_URI)${C_RESET}" ;;
*)
warn "Could not determine the revocation status from $OCSP_URI.
The responder could not be reached, or its answer could not be verified - a proxy or firewall
blocking outbound HTTP is the usual cause. That is a connectivity result and says nothing about
this release; every other check is unaffected." ;;
esac
fi

# ---------------------------------------------------------------------------
# Step 3 — Signer identity. Proves: WHO.
# Step 2 established that a certificate authority issued this certificate and verified whoever
# it was issued to. It did not establish who that was - this step does, and both are required.
# A certificate obtained by somebody else in their own name passes step 2 on its own merits.
#
# Tolerant of how the name is rendered, not of whose name it is: the issuing CA and the exact
# spelling legitimately change when the certificate is renewed, so neither is pinned.
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 3/5 — Signer identity ==="

LEAF_SUBJECT="$(openssl x509 -in "$LEAF" -noout -subject | sed 's/^subject=[[:space:]]*//')"
LEAF_ISSUER="$(openssl x509 -in "$LEAF" -noout -issuer | sed 's/^issuer=[[:space:]]*//')"
echo " Subject: $LEAF_SUBJECT"
echo " Issuer : $LEAF_ISSUER"

# Read from the structured DN, not the single-line rendering above: -nameopt multiline puts each
# attribute on its own line, so this depends neither on how openssl punctuates a DN nor on the
# order the attributes appear in.
LEAF_ORG="$(openssl x509 -in "$LEAF" -noout -subject -nameopt multiline 2>/dev/null \
| sed -n 's/^[[:space:]]*organizationName[[:space:]]*=[[:space:]]*//p' | head -1)"

# Compared with case, spacing, quoting and punctuation removed. Those are the certificate
# authority's rendering choices and they do change on renewal - "Omada A/S", "OMADA A/S" and
# "Omada AS" are the same organisation. Which organisation it is, though, is not theirs to
# re-render: it is what they verified against Omada's company registration.
#
# This is deliberately not a defence against a lookalike company name registered on purpose - no
# string comparison could be. What it rejects is an unrelated organisation: a certificate that
# somebody else obtained, entirely legitimately, in their own name.
LEAF_ORG_NORM="${LEAF_ORG,,}"
LEAF_ORG_NORM="${LEAF_ORG_NORM//[!a-z0-9]/}"

if [[ "$LEAF_ORG_NORM" == "omadaas" ]]; then
echo "${C_GREEN}✓ Organisation is Omada A/S${C_RESET}"
else
fail "This certificate was not issued to Omada A/S.
Its organisation is '${LEAF_ORG:-<none present>}'. Step 2 established only that a certificate
authority issued this certificate and verified whoever it was issued to - not that it was
issued to Omada, which is what this step checks.
Do not install this package; contact Omada support."
fi

# --- The published thumbprint ------------------------------------------------------------
# The only anchor in this script that Omada publishes OUTSIDE the release: it comes from the
# documentation portal, so whoever supplied the download cannot replace it, and it pins THIS
# certificate rather than a name a CA was willing to issue. The strongest check here; its shape was
# checked in the prerequisites. SHA-256 over the DER encoding - the same bytes Windows hashes, so
# both scripts and the portal refer to one value.
ACTUAL_TP="$(openssl x509 -in "$LEAF" -noout -fingerprint -sha256 2>/dev/null | sed 's/^.*=//')"
ACTUAL_TP="${ACTUAL_TP,,}"
ACTUAL_TP="${ACTUAL_TP//[!0-9a-f]/}"

if [[ "$ACTUAL_TP" == "$WANT_TP" ]]; then
echo "${C_GREEN}✓ Certificate matches the thumbprint you supplied${C_RESET}"
else
fail "The certificate does NOT match the thumbprint you supplied.
published: $WANT_TP
this file: $ACTUAL_TP
The certificate in this bundle is not the one Omada published, whatever else it checks out
against. Of everything this script does, this is the comparison whoever supplied the download
had no way to influence, so it is the one to act on.
Do not install this package; contact Omada support."
fi

# ---------------------------------------------------------------------------
# Step 4 — Signature, keyed from the certificate.
# Proves: CONSISTENCY only. On its own this proves nothing about origin - a signature can
# be made with any key, and shipped with the certificate that matches it. Step 2 is what
# makes this meaningful.
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 4/5 — Verifying signature (this may take a while for large files) ==="

# A truncated sidecar must produce the message below rather than an interpreter traceback, so
# python's own error is suppressed and an empty result is the signal.
KEY_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("keyId",""))' "$SIG_JSON" 2>/dev/null || true)"
[[ -n "$KEY_ID" ]] || fail "No keyId found in $SIG_JSON.
The file is incomplete or is not valid JSON. Re-download the full release bundle."
echo " Key ID: $KEY_ID"

case "$KEY_ID" in
"$EXPECTED_VAULT"*) : ;;
*) fail "Key identifier mismatch.
The signature reports that it was made in '$KEY_ID', which is not under Omada's
signing vault '$EXPECTED_VAULT'. Do not install this package; contact Omada support." ;;
esac

# From the certificate step 2 validated - never a separate file, which would put the anchor back
# in the download channel.
openssl x509 -in "$LEAF" -noout -pubkey > "$PUBKEY" 2>/dev/null \
|| fail "Could not extract the public key from $LEAF."

# Derived from the certificate, so a key-size change cannot leave a hardcoded number behind.
KEY_BITS="$(openssl pkey -pubin -in "$PUBKEY" -noout -text 2>/dev/null \
| sed -n 's/.*Public-Key: *(\([0-9]*\) bit).*/\1/p' | head -1)"

if ! python3 - "$SIG_JSON" "$SIG_DER" <<'EOF'
import base64, json, sys

try:
with open(sys.argv[1]) as f:
data = json.load(f)
encoded = data.get("signature", "")
if not encoded:
raise ValueError("no signature field")
sig = base64.b64decode(encoded)
except Exception:
sys.exit(1)

with open(sys.argv[2], "wb") as f:
f.write(sig)

print(f" Signature: {len(sig)} bytes")
EOF
then
fail "Could not read the signature from $SIG_JSON.
The file is incomplete, is not valid JSON, or its signature field is not valid base64.
Re-download the full release bundle."
fi

SIG_BYTES="$(wc -c < "$SIG_DER" | tr -d '[:space:]')"
if [[ -n "$KEY_BITS" ]]; then
EXPECTED_BYTES=$(( KEY_BITS / 8 ))
if [[ "$SIG_BYTES" != "$EXPECTED_BYTES" ]]; then
# An RS256 signature is exactly as long as the key's modulus, so a mismatch means the
# signature and the certificate do not belong together. Stopping here says why.
fail "Signature is $SIG_BYTES bytes but the certificate holds a ${KEY_BITS}-bit key
(expected $EXPECTED_BYTES bytes), so the signature was not made with the key in this
certificate. Do not install this package; contact Omada support."
fi
fi

openssl dgst -sha256 \
-verify "$PUBKEY" \
-signature "$SIG_DER" \
"$ZIP" \
|| fail "Signature verification FAILED.
The ZIP does not match the signature: it changed after it was signed, or the signature
belongs to a different file. Re-download the package; if the result is the same, contact
Omada support. Do not install this package until this check passes."

# ---------------------------------------------------------------------------
# Step 5 — Checksum. Proves: integrity, and catches a truncated download early.
# ---------------------------------------------------------------------------
echo ""
echo "=== Step 5/5 — Verifying checksum (this may take a while for large files) ==="

# sha256sum -c hashes the filename recorded inside the checksum file, resolved against the
# current directory, so it runs from where the release files are.
( cd -- "$ZIP_DIR" && sha256sum -c "$(basename -- "$CHECKSUM")" ) \
|| fail "Checksum verification FAILED.
The ZIP does not match the published checksum, or the checksum file names a different
file. Re-download the package, and confirm both files came from the same release."

echo ""
echo "${C_GREEN}=== ✓ All checks PASSED ===${C_RESET}"
echo "This release was signed by a certificate issued to Omada A/S, chaining to a root your"
echo "operating system trusts, at $SIGN_TIME, and the ZIP is byte-for-byte what was signed."