mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
Harden canary builds running.
This commit is contained in:
10
.github/telegram-bot-api/Dockerfile
vendored
10
.github/telegram-bot-api/Dockerfile
vendored
@@ -3,7 +3,9 @@
|
||||
# this container the bot token and the update files, so it must never be
|
||||
# replaced by a third-party image.
|
||||
|
||||
FROM ubuntu:22.04 AS build
|
||||
# The base image is pinned by digest (ubuntu:22.04 as of 2026-08-20) so
|
||||
# a rebuild of "the same" ref cannot silently pick up another base OS.
|
||||
FROM ubuntu:22.04@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc AS build
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates cmake g++ git gperf libssl-dev make zlib1g-dev \
|
||||
@@ -13,14 +15,16 @@ ARG TELEGRAM_BOT_API_REF
|
||||
RUN test -n "$TELEGRAM_BOT_API_REF" \
|
||||
&& git clone https://github.com/tdlib/telegram-bot-api.git /src \
|
||||
&& git -C /src checkout "$TELEGRAM_BOT_API_REF" \
|
||||
&& git -C /src submodule update --init --recursive
|
||||
&& git -C /src submodule update --init --recursive \
|
||||
&& echo "telegram-bot-api at $(git -C /src rev-parse HEAD)" \
|
||||
&& git -C /src submodule status --recursive
|
||||
|
||||
RUN cmake -S /src -B /build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/out \
|
||||
&& cmake --build /build --target install --parallel
|
||||
|
||||
FROM ubuntu:22.04
|
||||
FROM ubuntu:22.04@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates libssl3 zlib1g \
|
||||
|
||||
24
.github/workflows/canary-bot-api.yml
vendored
24
.github/workflows/canary-bot-api.yml
vendored
@@ -4,6 +4,10 @@
|
||||
# printed digest-pinned reference. Rebuild deliberately on upgrades,
|
||||
# never track a third-party image: the publish job hands this container
|
||||
# the bot token and every published update file.
|
||||
#
|
||||
# The job runs in the 'canary' environment so only the refs that
|
||||
# environment admits can dispatch it, exactly like the publish job that
|
||||
# consumes the image.
|
||||
|
||||
name: Canary Bot API image.
|
||||
|
||||
@@ -11,7 +15,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: tdlib/telegram-bot-api tag or commit to build
|
||||
description: full 40-hex tdlib/telegram-bot-api commit to build (the project has no tags; take the "Update version to X.Y" commit of a release)
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
@@ -22,14 +26,24 @@ jobs:
|
||||
|
||||
build:
|
||||
name: Build and push
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
runs-on: depot-ubuntu-latest-32
|
||||
environment: canary
|
||||
|
||||
steps:
|
||||
- name: Validate the ref.
|
||||
env:
|
||||
REF: ${{ inputs.ref }}
|
||||
run: |
|
||||
if ! [[ "$REF" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
echo "::error::Pass a full 40-hex commit, got '$REF'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Log in to GHCR.
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -37,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Build and push.
|
||||
id: push
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .github/telegram-bot-api
|
||||
push: true
|
||||
|
||||
462
.github/workflows/canary.yml
vendored
462
.github/workflows/canary.yml
vendored
@@ -44,7 +44,7 @@
|
||||
# every published update file.
|
||||
#
|
||||
# Public repository only:
|
||||
# secrets.CANARY_PUBLIC_CHANNEL_ID
|
||||
# vars.CANARY_PUBLIC_CHANNEL_ID
|
||||
# Numeric -100... public channel id for the Bot API calls.
|
||||
# vars.CANARY_PUBLIC_CHANNEL_USERNAME
|
||||
# Public channel username compiled into canary-public builds.
|
||||
@@ -54,13 +54,18 @@
|
||||
# Key Vault key name, must match the manifest id ("cp-2026a").
|
||||
#
|
||||
# Private repository only:
|
||||
# secrets.CANARY_PRIVATE_CHANNEL_ID
|
||||
# vars.CANARY_PRIVATE_CHANNEL_ID
|
||||
# Bare numeric id of the private channel (no -100 prefix), also
|
||||
# compiled into canary-private builds for discovery.
|
||||
# vars.CANARY_PRIVATE_METADATA_MSG_ID
|
||||
# Fixed id of the pinned metadata message in the private channel.
|
||||
# vars.CANARY_PRIVATE_SIGNING_KEY_ID
|
||||
# Key Vault key name, must match the manifest id ("cx-2026a").
|
||||
# vars.CANARY_ALLOW_UNSIGNED
|
||||
# Bring-up only: "1" publishes without Windows/macOS platform
|
||||
# signatures (the v2 envelope is still signed). Refused on the
|
||||
# public lane; remove it once KeyLocker and the Apple certificate
|
||||
# exist.
|
||||
#
|
||||
# Publishing no-ops cleanly while CANARY_BOT_TOKEN is absent, so the
|
||||
# workflow can run before the bots/channels/KeyLocker exist.
|
||||
@@ -76,17 +81,24 @@ concurrency:
|
||||
group: canary-publish
|
||||
cancel-in-progress: false
|
||||
|
||||
# The OIDC token that mints Key Vault signatures is granted per job to
|
||||
# the three packing jobs only, nothing else can request it.
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
|
||||
version:
|
||||
name: Version
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-latest
|
||||
environment: canary
|
||||
# Belt and braces next to the environment branch policy: the secrets
|
||||
# are never even requested from a run that is not a push to canary.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/canary'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
outputs:
|
||||
channel: ${{ steps.compute.outputs.channel }}
|
||||
@@ -95,10 +107,11 @@ jobs:
|
||||
counter: ${{ steps.compute.outputs.counter }}
|
||||
previous: ${{ steps.compute.outputs.previous }}
|
||||
publish: ${{ steps.compute.outputs.publish }}
|
||||
unsigned: ${{ steps.compute.outputs.unsigned }}
|
||||
|
||||
steps:
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# Full history: the changelog walks commits and the pipeline
|
||||
# must survive force-pushes and rebases of the canary branch.
|
||||
@@ -108,15 +121,26 @@ jobs:
|
||||
id: compute
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.CANARY_BOT_TOKEN }}
|
||||
PUBLIC_CHANNEL: ${{ secrets.CANARY_PUBLIC_CHANNEL_ID }}
|
||||
PRIVATE_CHANNEL: ${{ secrets.CANARY_PRIVATE_CHANNEL_ID }}
|
||||
PUBLIC_CHANNEL: ${{ vars.CANARY_PUBLIC_CHANNEL_ID }}
|
||||
PRIVATE_CHANNEL: ${{ vars.CANARY_PRIVATE_CHANNEL_ID }}
|
||||
PUBLIC_MSG_ID: ${{ vars.CANARY_METADATA_MSG_ID }}
|
||||
PRIVATE_MSG_ID: ${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}
|
||||
CHANNEL_OVERRIDE: ${{ vars.CANARY_CHANNEL }}
|
||||
ALLOW_UNSIGNED: ${{ vars.CANARY_ALLOW_UNSIGNED }}
|
||||
REPO_IS_PRIVATE: ${{ github.event.repository.private }}
|
||||
BOT_API_IMAGE: ${{ vars.CANARY_BOT_API_IMAGE }}
|
||||
TELEGRAM_API_ID: ${{ secrets.CANARY_API_ID }}
|
||||
TELEGRAM_API_HASH: ${{ secrets.CANARY_API_HASH }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
# A re-run of an older run would read the live counter and
|
||||
# republish old code as a newer version: only the current tip
|
||||
# of the canary branch may become a canary.
|
||||
if [ "$GITHUB_SHA" != "$(git rev-parse origin/canary)" ]; then
|
||||
echo "::error::$GITHUB_SHA is not the tip of canary, refusing to publish old code as a new version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHANNEL="$CHANNEL_OVERRIDE"
|
||||
if [ -z "$CHANNEL" ]; then
|
||||
if [ "$REPO_IS_PRIVATE" = "true" ]; then
|
||||
@@ -139,8 +163,10 @@ jobs:
|
||||
echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
|
||||
if [ "$CHANNEL" = "public" ]; then
|
||||
CHAT_ID="$PUBLIC_CHANNEL"
|
||||
MSG_ID="$PUBLIC_MSG_ID"
|
||||
else
|
||||
CHAT_ID="-100$PRIVATE_CHANNEL"
|
||||
MSG_ID="$PRIVATE_MSG_ID"
|
||||
fi
|
||||
|
||||
while IFS=' ' read -r name value; do
|
||||
@@ -155,15 +181,35 @@ jobs:
|
||||
fi
|
||||
echo "publish=$PUBLISH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Bring-up escape hatch for the private lane only: publish
|
||||
# builds without platform signatures (the v2 envelope is still
|
||||
# signed). The public lane never accepts it.
|
||||
UNSIGNED=false
|
||||
if [ "$ALLOW_UNSIGNED" = "1" ]; then
|
||||
if [ "$CHANNEL" != "private" ]; then
|
||||
echo "::error::CANARY_ALLOW_UNSIGNED is only honoured on the private lane."
|
||||
exit 1
|
||||
fi
|
||||
UNSIGNED=true
|
||||
echo "::warning::CANARY_ALLOW_UNSIGNED=1: this run publishes binaries WITHOUT platform signatures."
|
||||
fi
|
||||
echo "unsigned=$UNSIGNED" >> $GITHUB_OUTPUT
|
||||
|
||||
COUNTER=1
|
||||
PREVIOUS=""
|
||||
if [ "$PUBLISH" = "true" ] && [ -n "$CHAT_ID" ] && [ "$CHAT_ID" != "-100" ]; then
|
||||
if [ "$PUBLISH" = "true" ]; then
|
||||
if [ -z "$CHAT_ID" ] || [ "$CHAT_ID" = "-100" ] || [ -z "$MSG_ID" ]; then
|
||||
echo "::error::The lane's channel id and metadata message id are required when publishing."
|
||||
exit 1
|
||||
fi
|
||||
# The bot is logged out of the cloud Bot API (a requirement
|
||||
# for local server use), so the pinned metadata is read
|
||||
# through the same self-built local server the publish job
|
||||
# runs.
|
||||
if [ -z "$BOT_API_IMAGE" ]; then
|
||||
echo "::error::vars.CANARY_BOT_API_IMAGE is required when publishing."
|
||||
# runs. Bots cannot fetch a message by id, the pinned message
|
||||
# is the only way to read it back, so it MUST be the fixed
|
||||
# metadata message the clients use.
|
||||
if ! [[ "$BOT_API_IMAGE" =~ ^ghcr\.io/[A-Za-z0-9._/-]+@sha256:[0-9a-f]{64}$ ]]; then
|
||||
echo "::error::vars.CANARY_BOT_API_IMAGE must be a digest-pinned ghcr.io reference."
|
||||
exit 1
|
||||
fi
|
||||
echo "$GITHUB_TOKEN" | docker login ghcr.io \
|
||||
@@ -171,21 +217,50 @@ jobs:
|
||||
docker run -d --name bot-api -p 8081:8081 \
|
||||
-e TELEGRAM_API_ID -e TELEGRAM_API_HASH -e TELEGRAM_LOCAL=1 \
|
||||
"$BOT_API_IMAGE"
|
||||
READY=false
|
||||
for i in $(seq 1 30); do
|
||||
sleep 2
|
||||
if curl -sf "http://localhost:8081/bot$BOT_TOKEN/getMe" > /dev/null; then
|
||||
if curl -sf "http://localhost:8081/bot$BOT_TOKEN/getMe" | jq -e '.ok == true' > /dev/null; then
|
||||
READY=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
PINNED=$(curl -sf "http://localhost:8081/bot$BOT_TOKEN/getChat?chat_id=$CHAT_ID" \
|
||||
| jq -r '.result.pinned_message.text // empty')
|
||||
if [ "$READY" != "true" ]; then
|
||||
echo "::error::The local Bot API server did not come up."
|
||||
exit 1
|
||||
fi
|
||||
CHAT=$(curl -sf "http://localhost:8081/bot$BOT_TOKEN/getChat?chat_id=$CHAT_ID")
|
||||
docker rm -f bot-api > /dev/null
|
||||
if [ -n "$PINNED" ]; then
|
||||
if ! echo "$CHAT" | jq -e '.ok == true' > /dev/null; then
|
||||
echo "::error::getChat failed: $(echo "$CHAT" | jq -r '.description // "no response"')"
|
||||
exit 1
|
||||
fi
|
||||
PINNED_ID=$(echo "$CHAT" | jq -r '.result.pinned_message.message_id // empty')
|
||||
if [ "$PINNED_ID" != "$MSG_ID" ]; then
|
||||
echo "::error::The pinned message is '$PINNED_ID', expected the metadata message $MSG_ID; re-pin it."
|
||||
exit 1
|
||||
fi
|
||||
PINNED=$(echo "$CHAT" | jq -r '.result.pinned_message.text // empty')
|
||||
if ! echo "$PINNED" | jq -e 'type == "object"' > /dev/null; then
|
||||
echo "::error::The metadata message is not a JSON object, seed it with {} for a first publish."
|
||||
exit 1
|
||||
fi
|
||||
# No entry for this lane yet means the first publish of the
|
||||
# lane, anything else must continue the counter sequence.
|
||||
if echo "$PINNED" | jq -e ".channels.\"canary-$CHANNEL\"" > /dev/null; then
|
||||
OLD_BASE=$(echo "$PINNED" | jq -r ".channels.\"canary-$CHANNEL\".base // 0")
|
||||
OLD_COUNTER=$(echo "$PINNED" | jq -r ".channels.\"canary-$CHANNEL\".counter // 0")
|
||||
PREVIOUS=$(echo "$PINNED" | jq -r ".channels.\"canary-$CHANNEL\".commit // empty")
|
||||
if [ "$OLD_BASE" = "$BASE" ]; then
|
||||
COUNTER=$((OLD_COUNTER + 1))
|
||||
elif [ "$OLD_BASE" -gt "$BASE" ]; then
|
||||
echo "::error::The channel is at base $OLD_BASE, this branch builds $BASE; a lower base never publishes."
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$PREVIOUS" ] && git cat-file -e "$PREVIOUS^{commit}" 2>/dev/null \
|
||||
&& ! git merge-base --is-ancestor "$PREVIOUS" HEAD; then
|
||||
echo "::error::The published commit $PREVIOUS is not an ancestor of HEAD, refusing to publish a rollback as an update."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -195,10 +270,14 @@ jobs:
|
||||
|
||||
windows:
|
||||
name: Windows x64 (${{ needs.version.outputs.channel }})
|
||||
runs-on: depot-windows-latest-16
|
||||
runs-on: depot-windows-latest-32
|
||||
needs: version
|
||||
environment: canary
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
# win-arm64 is phase 2: add an arch matrix here together with the
|
||||
# windows-11-arm runner and the VS ARM64 build tools steps from
|
||||
# win.yml when the canary channels grow an arm feed.
|
||||
@@ -222,12 +301,41 @@ jobs:
|
||||
shell: bash
|
||||
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Git auth through the TBuild symlink.
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# actions/checkout persists its auth header in the repository's
|
||||
# local git config, which does not take effect for a checkout
|
||||
# path that goes through the TBuild directory symlink: on the
|
||||
# public repository the fetch needs no credentials, on the
|
||||
# private one it fails with "could not read Username". The same
|
||||
# header passed through the environment applies to every git
|
||||
# call of the checkout step regardless of the path.
|
||||
BASIC=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 -w0)
|
||||
echo "::add-mask::$BASIC"
|
||||
echo "GIT_AUTH_HEADER=AUTHORIZATION: basic $BASIC" >> $GITHUB_ENV
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
env:
|
||||
GIT_CONFIG_COUNT: 1
|
||||
GIT_CONFIG_KEY_0: http.https://github.com/.extraheader
|
||||
GIT_CONFIG_VALUE_0: ${{ env.GIT_AUTH_HEADER }}
|
||||
with:
|
||||
submodules: recursive
|
||||
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}
|
||||
|
||||
- name: Submodules.
|
||||
shell: bash
|
||||
run: |
|
||||
# Separate from the clone on purpose: with submodules enabled
|
||||
# actions/checkout adds its own Authorization header on top of
|
||||
# the one above and GitHub rejects the duplicate. The submodules
|
||||
# are public repositories and need no credentials.
|
||||
cd $TBUILD/$REPO_NAME
|
||||
git submodule update --init --recursive --depth=1
|
||||
|
||||
- name: Read canary configuration.
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -239,7 +347,7 @@ jobs:
|
||||
else
|
||||
echo "CANARY_TAG=canarypriv" >> $GITHUB_ENV
|
||||
echo "CANARY_KEY_ID=${{ vars.CANARY_PRIVATE_SIGNING_KEY_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ secrets.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ vars.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: First set up.
|
||||
@@ -259,7 +367,10 @@ jobs:
|
||||
git config --global user.email "you@example.com"
|
||||
git config --global user.name "Sample"
|
||||
|
||||
- uses: Eden-CI/msvc-dev-cmd@master
|
||||
# Pinned to a commit (master as of 2026-04-01): this job holds the
|
||||
# platform-signing credentials and the OIDC token, a floating tag
|
||||
# would let a third party run code next to them.
|
||||
- uses: Eden-CI/msvc-dev-cmd@1bd71f95d6f3d1b2b3395b335cc63bcdc90cf223
|
||||
name: Native Tools Command Prompt.
|
||||
with:
|
||||
arch: x64
|
||||
@@ -272,14 +383,14 @@ jobs:
|
||||
nuget sources Add -Source https://api.nuget.org/v3/index.json & exit 0
|
||||
|
||||
- name: ThirdParty cache.
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: ${{ env.TBUILD }}\ThirdParty
|
||||
key: ${{ runner.OS }}-${{ runner.arch }}-third-party-${{ env.CACHE_KEY }}
|
||||
restore-keys: ${{ runner.OS }}-${{ runner.arch }}-third-party-
|
||||
|
||||
- name: Libraries cache.
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.LibrariesPath }}\*
|
||||
@@ -291,7 +402,7 @@ jobs:
|
||||
restore-keys: ${{ runner.OS }}-x64-libs-v2rel-
|
||||
|
||||
- name: Qt cache.
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: |
|
||||
${{ env.LibrariesPath }}\[qQ]t[_-]*
|
||||
@@ -332,7 +443,6 @@ jobs:
|
||||
-D TDESKTOP_API_ID=${{ secrets.CANARY_API_ID }} ^
|
||||
-D TDESKTOP_API_HASH=${{ secrets.CANARY_API_HASH }} ^
|
||||
-D CMAKE_CONFIGURATION_TYPES=Release ^
|
||||
-D CMAKE_MSVC_DEBUG_INFORMATION_FORMAT= ^
|
||||
-D DESKTOP_APP_SPECIAL_TARGET=win64 ^
|
||||
-D DESKTOP_APP_ENABLE_LTO=ON ^
|
||||
-D DESKTOP_APP_DISABLE_AUTOUPDATE=OFF ^
|
||||
@@ -372,6 +482,11 @@ jobs:
|
||||
run: |
|
||||
cd $TBUILD/$REPO_NAME/out/Release
|
||||
if [ -z "$SM_API_KEY" ]; then
|
||||
if [ "${{ needs.version.outputs.publish }}" = "true" ] \
|
||||
&& [ "${{ needs.version.outputs.unsigned }}" != "true" ]; then
|
||||
echo "::error::Publishing requires the KeyLocker secrets, unsigned binaries never ship."
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::KeyLocker secrets absent, leaving binaries unsigned."
|
||||
exit 0
|
||||
fi
|
||||
@@ -381,13 +496,15 @@ jobs:
|
||||
export SM_CLIENT_CERT_FILE=/tmp/keylocker.p12
|
||||
smctl sign --keypair-alias "$SM_KEYPAIR_ALIAS" --input Telegram.exe
|
||||
smctl sign --keypair-alias "$SM_KEYPAIR_ALIAS" --input Updater.exe
|
||||
# TODO(canary-infra): verify both signatures here (signtool or
|
||||
# smctl) — the publish job can only re-check the portable's
|
||||
# Telegram.exe, Updater.exe travels inside the update envelope.
|
||||
# Both binaries are verified here, on the exact bytes that get
|
||||
# packed: Updater.exe travels only inside the update envelope,
|
||||
# the publish job can re-check just the portable's Telegram.exe.
|
||||
signtool verify /pa /all Telegram.exe
|
||||
signtool verify /pa /all Updater.exe
|
||||
|
||||
- name: Azure login for update signing.
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -419,8 +536,11 @@ jobs:
|
||||
-keys-loc ../../Telegram/Resources/update \
|
||||
-unsigned update-win-x64-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned \
|
||||
-embed-signatures $CANARY_KEY_ID:canary.sig
|
||||
rm update-win-x64-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned signing-input.bin canary.sig
|
||||
UPDATE=update-win-x64-$CANARY_TAG-$BASE-$CANARY_COUNTER
|
||||
else
|
||||
echo "::warning::No publish secrets, keeping the unsigned envelope only."
|
||||
UPDATE=update-win-x64-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned
|
||||
fi
|
||||
|
||||
# The first-install artifact is a portable-style archive (like
|
||||
@@ -433,15 +553,26 @@ jobs:
|
||||
cp Telegram.exe portable/Telegram/
|
||||
(cd portable && 7z a -mx9 ../$PORTABLE Telegram/)
|
||||
|
||||
mkdir artifact
|
||||
mv update-win-x64-$CANARY_TAG-* artifact/
|
||||
mv $PORTABLE artifact/
|
||||
mkdir -p artifact/update artifact/portable
|
||||
mv $UPDATE artifact/update/
|
||||
mv $PORTABLE artifact/portable/
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
name: Upload artifact.
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the update.
|
||||
with:
|
||||
name: canary-win64
|
||||
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\artifact\
|
||||
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\artifact\update\
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
# The portable is its own artifact so its link can be handed to a
|
||||
# user in an issue; on the private lane it is transport to the
|
||||
# publish job only.
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the portable.
|
||||
with:
|
||||
name: canary-win64-portable
|
||||
path: ${{ env.TBUILD }}\${{ env.REPO_NAME }}\out\Release\artifact\portable\
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
macos:
|
||||
name: macOS universal (${{ needs.version.outputs.channel }})
|
||||
@@ -449,6 +580,10 @@ jobs:
|
||||
needs: version
|
||||
environment: canary
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
PREPARE_PATH: "Telegram/build/prepare/prepare.py"
|
||||
|
||||
@@ -457,7 +592,7 @@ jobs:
|
||||
run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
submodules: recursive
|
||||
path: ${{ env.REPO_NAME }}
|
||||
@@ -472,7 +607,7 @@ jobs:
|
||||
else
|
||||
echo "CANARY_TAG=canarypriv" >> $GITHUB_ENV
|
||||
echo "CANARY_KEY_ID=${{ vars.CANARY_PRIVATE_SIGNING_KEY_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ secrets.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ vars.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: First set up.
|
||||
@@ -488,7 +623,7 @@ jobs:
|
||||
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
|
||||
|
||||
- name: Libraries cache.
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: |
|
||||
Libraries
|
||||
@@ -522,8 +657,7 @@ jobs:
|
||||
./configure.sh \
|
||||
-D CMAKE_CONFIGURATION_TYPES=Release \
|
||||
-D CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=NO \
|
||||
-D CMAKE_POLICY_DEFAULT_CMP0069=NEW \
|
||||
-D CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON \
|
||||
-D DESKTOP_APP_ENABLE_LTO=ON \
|
||||
-D CMAKE_OSX_ARCHITECTURES="x86_64;arm64" \
|
||||
-D TDESKTOP_API_ID=${{ secrets.CANARY_API_ID }} \
|
||||
-D TDESKTOP_API_HASH=${{ secrets.CANARY_API_HASH }} \
|
||||
@@ -587,6 +721,11 @@ jobs:
|
||||
run: |
|
||||
cd $REPO_NAME/out/Release
|
||||
if [ -z "$CERTIFICATE_P12_B64" ]; then
|
||||
if [ "${{ needs.version.outputs.publish }}" = "true" ] \
|
||||
&& [ "${{ needs.version.outputs.unsigned }}" != "true" ]; then
|
||||
echo "::error::Publishing requires the signing certificate, unsigned apps never ship."
|
||||
exit 1
|
||||
fi
|
||||
echo "::warning::No signing certificate, leaving the apps unsigned."
|
||||
exit 0
|
||||
fi
|
||||
@@ -611,12 +750,14 @@ jobs:
|
||||
--team-id "$NOTARY_TEAM_ID" \
|
||||
--password "$NOTARY_PASSWORD"
|
||||
xcrun stapler staple "$BUNDLE"
|
||||
xcrun stapler validate "$BUNDLE"
|
||||
spctl --assess --type execute --verbose=2 "$BUNDLE"
|
||||
rm notarize.zip
|
||||
done
|
||||
|
||||
- name: Azure login for update signing.
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -654,10 +795,12 @@ jobs:
|
||||
-keys-loc ../../../Telegram/Resources/update \
|
||||
-unsigned update-mac-$SHORT-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned \
|
||||
-embed-signatures $CANARY_KEY_ID:canary.sig
|
||||
rm update-mac-$SHORT-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned signing-input.bin canary.sig
|
||||
mv update-mac-$SHORT-$CANARY_TAG-$BASE-$CANARY_COUNTER ../
|
||||
else
|
||||
echo "::warning::No publish secrets, keeping the unsigned envelope only."
|
||||
mv update-mac-$SHORT-$CANARY_TAG-$BASE-$CANARY_COUNTER.unsigned ../
|
||||
fi
|
||||
mv update-mac-$SHORT-$CANARY_TAG-* ../
|
||||
cd ..
|
||||
rm -rf update_pack
|
||||
done
|
||||
@@ -670,28 +813,40 @@ jobs:
|
||||
cp -R Telegram.app portable/Telegram/
|
||||
(cd portable && zip -q -r ../$PORTABLE Telegram)
|
||||
|
||||
mkdir artifact
|
||||
mv update-mac-*-$CANARY_TAG-* artifact/
|
||||
mv $PORTABLE artifact/
|
||||
mkdir -p artifact/update artifact/portable
|
||||
mv update-mac-*-$CANARY_TAG-* artifact/update/
|
||||
mv $PORTABLE artifact/portable/
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
name: Upload artifact.
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the updates.
|
||||
with:
|
||||
name: canary-mac
|
||||
path: ${{ env.REPO_NAME }}/out/Release/artifact/
|
||||
path: ${{ env.REPO_NAME }}/out/Release/artifact/update/
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the portable.
|
||||
with:
|
||||
name: canary-mac-portable
|
||||
path: ${{ env.REPO_NAME }}/out/Release/artifact/portable/
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
linux:
|
||||
name: Linux x64 (${{ needs.version.outputs.channel }})
|
||||
runs-on: depot-ubuntu-latest-16
|
||||
runs-on: depot-ubuntu-latest-32
|
||||
needs: version
|
||||
environment: canary
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
IMAGE_TAG: tdesktop:centos_env
|
||||
|
||||
steps:
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
@@ -705,16 +860,18 @@ jobs:
|
||||
else
|
||||
echo "CANARY_TAG=canarypriv" >> $GITHUB_ENV
|
||||
echo "CANARY_KEY_ID=${{ vars.CANARY_PRIVATE_SIGNING_KEY_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ secrets.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
echo "CANARY_DEFINES=-D TDESKTOP_UPDATE_CHANNEL=canary-private -D TDESKTOP_CANARY_PRIVATE_CHANNEL_ID=${{ vars.CANARY_PRIVATE_CHANNEL_ID }} -D TDESKTOP_CANARY_METADATA_MSG_ID=${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: First set up.
|
||||
run: |
|
||||
# The template only needs Jinja2: the distro package replaces
|
||||
# the curl-piped poetry installer linux.yml uses, so no code
|
||||
# from an unpinned URL runs in a job that later signs.
|
||||
sudo apt update
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
sudo apt install -y python3-jinja2
|
||||
cd Telegram/build/docker/centos_env
|
||||
poetry install
|
||||
DOCKERFILE=$(DEBUG= poetry run gen_dockerfile)
|
||||
DOCKERFILE=$(DEBUG= python3 gen_dockerfile.py)
|
||||
echo "$DOCKERFILE" > Dockerfile
|
||||
rm -rf __pycache__
|
||||
|
||||
@@ -725,11 +882,11 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx.
|
||||
id: setup-buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
|
||||
- name: Libraries cache.
|
||||
id: cache-libs
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: |
|
||||
${{ runner.temp }}/.buildx-cache
|
||||
@@ -746,7 +903,7 @@ jobs:
|
||||
skip-extraction: ${{ steps.cache-libs.outputs.cache-hit }}
|
||||
|
||||
- name: Libraries.
|
||||
uses: docker/build-push-action@v7
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: Telegram/build/docker/centos_env
|
||||
load: true
|
||||
@@ -781,7 +938,7 @@ jobs:
|
||||
$IMAGE_TAG \
|
||||
/usr/src/tdesktop/Telegram/build/docker/centos_env/build.sh \
|
||||
-D CMAKE_CONFIGURATION_TYPES=Release \
|
||||
-D CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=ON \
|
||||
-D DESKTOP_APP_ENABLE_LTO=ON \
|
||||
-D TDESKTOP_API_ID=${{ secrets.CANARY_API_ID }} \
|
||||
-D TDESKTOP_API_HASH=${{ secrets.CANARY_API_HASH }} \
|
||||
-D DESKTOP_APP_SPECIAL_TARGET=linux \
|
||||
@@ -804,9 +961,17 @@ jobs:
|
||||
# TODO(canary-infra): upload symbols/ to R2 (see the Windows job).
|
||||
../../Telegram/build/minidebug.sh Telegram
|
||||
|
||||
- name: Verify the update trust chain.
|
||||
run: |
|
||||
# The focused verification test and the Packer round-trip run
|
||||
# against the exact binaries built for this canary, including
|
||||
# the committed trust files against the pinned root.
|
||||
out/Release/test_update_verify
|
||||
Telegram/build/canary_test_fixtures.sh out/Release/Packer "$RUNNER_TEMP/fixtures"
|
||||
|
||||
- name: Azure login for update signing.
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -831,8 +996,11 @@ jobs:
|
||||
-keys-loc ../../Telegram/Resources/update \
|
||||
-unsigned update-linux-x64-$CANARY_TAG-${{ needs.version.outputs.base }}-$CANARY_COUNTER.unsigned \
|
||||
-embed-signatures $CANARY_KEY_ID:canary.sig
|
||||
rm update-linux-x64-$CANARY_TAG-${{ needs.version.outputs.base }}-$CANARY_COUNTER.unsigned signing-input.bin canary.sig
|
||||
UPDATE=update-linux-x64-$CANARY_TAG-${{ needs.version.outputs.base }}-$CANARY_COUNTER
|
||||
else
|
||||
echo "::warning::No publish secrets, keeping the unsigned envelope only."
|
||||
UPDATE=update-linux-x64-$CANARY_TAG-${{ needs.version.outputs.base }}-$CANARY_COUNTER.unsigned
|
||||
fi
|
||||
|
||||
# Portable-style first-install archive, tdata forced next to
|
||||
@@ -843,23 +1011,35 @@ jobs:
|
||||
cp Telegram Updater portable/Telegram/
|
||||
tar -cJf $PORTABLE -C portable Telegram
|
||||
|
||||
mkdir artifact
|
||||
mv update-linux-x64-$CANARY_TAG-* artifact/
|
||||
mv $PORTABLE artifact/
|
||||
mkdir -p artifact/update artifact/portable
|
||||
mv $UPDATE artifact/update/
|
||||
mv $PORTABLE artifact/portable/
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
name: Upload artifact.
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the update.
|
||||
with:
|
||||
name: canary-linux
|
||||
path: out/Release/artifact/
|
||||
path: out/Release/artifact/update/
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
name: Upload the portable.
|
||||
with:
|
||||
name: canary-linux-portable
|
||||
path: out/Release/artifact/portable/
|
||||
retention-days: ${{ needs.version.outputs.channel == 'public' && 90 || 1 }}
|
||||
|
||||
publish:
|
||||
name: Publish (${{ needs.version.outputs.channel }})
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-latest
|
||||
needs: [version, windows, macos, linux]
|
||||
if: needs.version.outputs.publish == 'true'
|
||||
environment: canary
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
services:
|
||||
# Built from a pinned tdlib/telegram-bot-api ref by the
|
||||
# canary-bot-api.yml workflow and referenced by digest: this
|
||||
@@ -883,25 +1063,40 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone.
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download artifacts.
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Wait for the Bot API server.
|
||||
run: |
|
||||
# GitHub only waits for the container process, not for tdlib to
|
||||
# accept requests.
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf "$BOT_API/bot$BOT_TOKEN/getMe" | jq -e '.ok == true' > /dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "::error::The local Bot API server did not come up."
|
||||
exit 1
|
||||
|
||||
- name: Verify platform signatures.
|
||||
if: needs.version.outputs.unsigned != 'true'
|
||||
run: |
|
||||
# Publishing unsigned binaries is never allowed: this is a hard
|
||||
# gate, not a warning. The binaries inside the update envelopes
|
||||
# were verified right after signing in the build jobs; here the
|
||||
# were verified right after signing in the build jobs (signtool
|
||||
# on Windows, codesign + stapler + spctl on macOS); here the
|
||||
# portable archives are re-checked as the publish-side witness.
|
||||
sudo apt-get update && sudo apt-get install -y osslsigncode
|
||||
FAILED=0
|
||||
|
||||
unzip -q artifacts/canary-win64/portable-win-x64-*.zip -d /tmp/winapp
|
||||
unzip -q artifacts/canary-win64-portable/portable-win-x64-*.zip -d /tmp/winapp
|
||||
if ! osslsigncode verify /tmp/winapp/Telegram/Telegram.exe; then
|
||||
echo "::error::The portable Telegram.exe is not Authenticode-signed."
|
||||
FAILED=1
|
||||
@@ -910,7 +1105,7 @@ jobs:
|
||||
# TODO(canary-infra): pin an apple-codesign (rcodesign) release
|
||||
# for full macOS signature+staple verification on Linux:
|
||||
# rcodesign verify /tmp/macapp/Telegram/Telegram.app
|
||||
unzip -q artifacts/canary-mac/portable-mac-universal-*.zip -d /tmp/macapp
|
||||
unzip -q artifacts/canary-mac-portable/portable-mac-universal-*.zip -d /tmp/macapp
|
||||
if [ ! -d "/tmp/macapp/Telegram/Telegram.app/Contents/_CodeSignature" ]; then
|
||||
echo "::error::The portable Telegram.app has no code signature."
|
||||
FAILED=1
|
||||
@@ -921,8 +1116,8 @@ jobs:
|
||||
- name: Publish the channel.
|
||||
env:
|
||||
CHANNEL: ${{ needs.version.outputs.channel }}
|
||||
PUBLIC_CHANNEL: ${{ secrets.CANARY_PUBLIC_CHANNEL_ID }}
|
||||
PRIVATE_CHANNEL: ${{ secrets.CANARY_PRIVATE_CHANNEL_ID }}
|
||||
PUBLIC_CHANNEL: ${{ vars.CANARY_PUBLIC_CHANNEL_ID }}
|
||||
PRIVATE_CHANNEL: ${{ vars.CANARY_PRIVATE_CHANNEL_ID }}
|
||||
PUBLIC_MSG_ID: ${{ vars.CANARY_METADATA_MSG_ID }}
|
||||
PRIVATE_MSG_ID: ${{ vars.CANARY_PRIVATE_METADATA_MSG_ID }}
|
||||
run: |
|
||||
@@ -933,11 +1128,29 @@ jobs:
|
||||
CHAT_ID="-100$PRIVATE_CHANNEL"
|
||||
MSG_ID="$PRIVATE_MSG_ID"
|
||||
fi
|
||||
BASE="${{ needs.version.outputs.base }}"
|
||||
COUNTER="${{ needs.version.outputs.counter }}"
|
||||
PREVIOUS="${{ needs.version.outputs.previous }}"
|
||||
TAG=canarypub
|
||||
if [ "$CHANNEL" = "private" ]; then TAG=canarypriv; fi
|
||||
|
||||
call() {
|
||||
local METHOD="$1"
|
||||
shift
|
||||
local RESPONSE
|
||||
RESPONSE=$(curl -sf "$BOT_API/bot$BOT_TOKEN/$METHOD" "$@")
|
||||
if ! echo "$RESPONSE" | jq -e '.ok == true' > /dev/null; then
|
||||
echo "::error::$METHOD failed: $(echo "$RESPONSE" | jq -r '.description // "no response"')"
|
||||
return 1
|
||||
fi
|
||||
echo "$RESPONSE"
|
||||
}
|
||||
|
||||
CAPTION=$({
|
||||
echo "Canary #$COUNTER · ${{ needs.version.outputs.commit }}"
|
||||
if [ "${{ needs.version.outputs.unsigned }}" = "true" ]; then
|
||||
echo "UNSIGNED test build: no Authenticode / notarization."
|
||||
fi
|
||||
echo ""
|
||||
if [ -n "$PREVIOUS" ] && git cat-file -e "$PREVIOUS^{commit}" 2>/dev/null; then
|
||||
git log --no-merges --pretty=format:'• %s' "$PREVIOUS..HEAD" | head -20
|
||||
@@ -946,42 +1159,56 @@ jobs:
|
||||
fi
|
||||
} | head -c 1000)
|
||||
|
||||
# Exact names only: the build jobs delete the .unsigned
|
||||
# intermediates, a publish run never guesses from a glob.
|
||||
declare -A FILES
|
||||
FILES[win64]=$(ls artifacts/canary-win64/update-win-x64-* | head -1)
|
||||
FILES[mac]=$(ls artifacts/canary-mac/update-mac-x64-* | head -1)
|
||||
FILES[armac]=$(ls artifacts/canary-mac/update-mac-arm-* | head -1)
|
||||
FILES[linux]=$(ls artifacts/canary-linux/update-linux-x64-* | head -1)
|
||||
FILES[win64]=artifacts/canary-win64/update-win-x64-$TAG-$BASE-$COUNTER
|
||||
FILES[mac]=artifacts/canary-mac/update-mac-x64-$TAG-$BASE-$COUNTER
|
||||
FILES[armac]=artifacts/canary-mac/update-mac-arm-$TAG-$BASE-$COUNTER
|
||||
FILES[linux]=artifacts/canary-linux/update-linux-x64-$TAG-$BASE-$COUNTER
|
||||
declare -A PORTABLES
|
||||
PORTABLES[win64]=artifacts/canary-win64-portable/portable-win-x64-$TAG-$BASE-$COUNTER.zip
|
||||
PORTABLES[mac]=artifacts/canary-mac-portable/portable-mac-universal-$TAG-$BASE-$COUNTER.zip
|
||||
PORTABLES[linux]=artifacts/canary-linux-portable/portable-linux-x64-$TAG-$BASE-$COUNTER.tar.xz
|
||||
for FILE in "${FILES[@]}" "${PORTABLES[@]}"; do
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "::error::$FILE is missing, refusing to publish."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The metadata is read back first so that entries other lanes
|
||||
# or a human wrote (the dormancy-rescue 'stable' entry of a
|
||||
# retired lane) survive: only this lane's entry and the trust
|
||||
# material are replaced.
|
||||
CHAT=$(call getChat -F chat_id="$CHAT_ID")
|
||||
PINNED_ID=$(echo "$CHAT" | jq -r '.result.pinned_message.message_id // empty')
|
||||
if [ "$PINNED_ID" != "$MSG_ID" ]; then
|
||||
echo "::error::The pinned message is '$PINNED_ID', expected the metadata message $MSG_ID."
|
||||
exit 1
|
||||
fi
|
||||
CURRENT=$(echo "$CHAT" | jq -r '.result.pinned_message.text // empty')
|
||||
if ! echo "$CURRENT" | jq -e 'type == "object"' > /dev/null; then
|
||||
echo "::error::The metadata message is not a JSON object."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A POSTS
|
||||
for PLATFORM in win64 mac armac linux; do
|
||||
FILE=${FILES[$PLATFORM]}
|
||||
if [ -z "$FILE" ] || [[ "$FILE" == *.unsigned ]]; then
|
||||
echo "::error::$PLATFORM update is missing or unsigned, refusing to publish."
|
||||
exit 1
|
||||
fi
|
||||
RESPONSE=$(curl -sf "$BOT_API/bot$BOT_TOKEN/sendDocument" \
|
||||
RESPONSE=$(call sendDocument \
|
||||
-F chat_id="$CHAT_ID" \
|
||||
-F document=@"$FILE" \
|
||||
-F document=@"${FILES[$PLATFORM]}" \
|
||||
-F caption="$CAPTION")
|
||||
POSTS[$PLATFORM]=$(echo "$RESPONSE" | jq -r '.result.message_id')
|
||||
POSTS[$PLATFORM]=$(echo "$RESPONSE" | jq -e -r '.result.message_id | numbers')
|
||||
echo "$PLATFORM -> post ${POSTS[$PLATFORM]}"
|
||||
done
|
||||
|
||||
# The portable archives are for first installs, posted as plain
|
||||
# documents and not referenced from the metadata.
|
||||
declare -A PORTABLES
|
||||
PORTABLES[win64]=$(ls artifacts/canary-win64/portable-win-x64-* | head -1)
|
||||
PORTABLES[mac]=$(ls artifacts/canary-mac/portable-mac-universal-* | head -1)
|
||||
PORTABLES[linux]=$(ls artifacts/canary-linux/portable-linux-x64-* | head -1)
|
||||
for PLATFORM in win64 mac linux; do
|
||||
PORTABLE=${PORTABLES[$PLATFORM]}
|
||||
if [ -z "$PORTABLE" ]; then
|
||||
echo "::error::$PLATFORM portable archive is missing."
|
||||
exit 1
|
||||
fi
|
||||
curl -sf "$BOT_API/bot$BOT_TOKEN/sendDocument" \
|
||||
call sendDocument \
|
||||
-F chat_id="$CHAT_ID" \
|
||||
-F document=@"$PORTABLE" \
|
||||
-F document=@"${PORTABLES[$PLATFORM]}" \
|
||||
-F caption="Portable, $CAPTION" > /dev/null
|
||||
echo "$PLATFORM portable posted."
|
||||
done
|
||||
@@ -989,38 +1216,39 @@ jobs:
|
||||
MANIFEST_B64=$(base64 -w0 Telegram/Resources/update/manifest.min.json)
|
||||
MANIFEST_SIG_B64=$(base64 -w0 Telegram/Resources/update/manifest.sig)
|
||||
|
||||
NEW=$(jq -n \
|
||||
NEW=$(echo "$CURRENT" | jq \
|
||||
--arg manifest "$MANIFEST_B64" \
|
||||
--arg manifest_sig "$MANIFEST_SIG_B64" \
|
||||
--arg commit "${{ needs.version.outputs.commit }}" \
|
||||
--argjson base "${{ needs.version.outputs.base }}" \
|
||||
--argjson base "$BASE" \
|
||||
--argjson counter "$COUNTER" \
|
||||
--argjson win64 "${POSTS[win64]}" \
|
||||
--argjson mac "${POSTS[mac]}" \
|
||||
--argjson armac "${POSTS[armac]}" \
|
||||
--argjson linux "${POSTS[linux]}" \
|
||||
"{
|
||||
". + {
|
||||
format: 1,
|
||||
manifest: \$manifest,
|
||||
manifest_sig: \$manifest_sig,
|
||||
channels: {
|
||||
\"canary-$CHANNEL\": {
|
||||
base: \$base,
|
||||
counter: \$counter,
|
||||
commit: \$commit,
|
||||
posts: {
|
||||
win64: \$win64,
|
||||
mac: \$mac,
|
||||
armac: \$armac,
|
||||
linux: \$linux
|
||||
}
|
||||
manifest_sig: \$manifest_sig
|
||||
} | .channels = ((.channels // {}) + {
|
||||
\"canary-$CHANNEL\": {
|
||||
base: \$base,
|
||||
counter: \$counter,
|
||||
commit: \$commit,
|
||||
posts: {
|
||||
win64: \$win64,
|
||||
mac: \$mac,
|
||||
armac: \$armac,
|
||||
linux: \$linux
|
||||
}
|
||||
}
|
||||
}")
|
||||
curl -sf "$BOT_API/bot$BOT_TOKEN/editMessageText" \
|
||||
})")
|
||||
if [ "${#NEW}" -gt 4096 ]; then
|
||||
echo "::error::The metadata message would exceed 4096 characters, prune the manifest or old entries."
|
||||
exit 1
|
||||
fi
|
||||
call editMessageText \
|
||||
-F chat_id="$CHAT_ID" \
|
||||
-F message_id="$MSG_ID" \
|
||||
--form-string text="$NEW" || {
|
||||
echo "::error::Could not edit the metadata message $MSG_ID."
|
||||
exit 1
|
||||
}
|
||||
--form-string text="$NEW" > /dev/null
|
||||
echo "Metadata message $MSG_ID updated to canary-$CHANNEL $BASE #$COUNTER."
|
||||
|
||||
3
.github/workflows/linux.yml
vendored
3
.github/workflows/linux.yml
vendored
@@ -2,6 +2,9 @@ name: Linux.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Pushes to canary run only canary.yml, see there.
|
||||
branches-ignore:
|
||||
- canary
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
3
.github/workflows/mac.yml
vendored
3
.github/workflows/mac.yml
vendored
@@ -2,6 +2,9 @@ name: MacOS.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Pushes to canary run only canary.yml, see there.
|
||||
branches-ignore:
|
||||
- canary
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
3
.github/workflows/mac_packaged.yml
vendored
3
.github/workflows/mac_packaged.yml
vendored
@@ -2,6 +2,9 @@ name: MacOS Packaged.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Pushes to canary run only canary.yml, see there.
|
||||
branches-ignore:
|
||||
- canary
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
3
.github/workflows/snap.yml
vendored
3
.github/workflows/snap.yml
vendored
@@ -2,6 +2,9 @@ name: Snap.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Pushes to canary run only canary.yml, see there.
|
||||
branches-ignore:
|
||||
- canary
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
3
.github/workflows/win.yml
vendored
3
.github/workflows/win.yml
vendored
@@ -2,6 +2,9 @@ name: Windows.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Pushes to canary run only canary.yml, see there.
|
||||
branches-ignore:
|
||||
- canary
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
@@ -56,6 +56,10 @@ endif()
|
||||
include(cmake/generate_update_keys.cmake)
|
||||
generate_update_keys(Telegram ${res_loc}/update)
|
||||
|
||||
if (DESKTOP_APP_TEST_APPS OR DESKTOP_APP_SPECIAL_TARGET)
|
||||
include(cmake/test_update_verify.cmake)
|
||||
endif()
|
||||
|
||||
set_target_properties(Telegram PROPERTIES AUTOMOC ON)
|
||||
|
||||
target_link_libraries(Telegram
|
||||
|
||||
@@ -27,8 +27,7 @@ QString V2LocalKeyId;
|
||||
QString V2SigningInputFile;
|
||||
QString V2UnsignedFile;
|
||||
std::vector<std::pair<QString, QString>> V2EmbedSignatures;
|
||||
QString V2Os;
|
||||
QString V2Arch;
|
||||
Core::Updates::Target V2Target;
|
||||
|
||||
const char *PublicKey = "\
|
||||
-----BEGIN RSA PUBLIC KEY-----\n\
|
||||
@@ -201,8 +200,8 @@ void AppendLeU64(QByteArray &to, quint64 value) {
|
||||
quint32 base,
|
||||
quint32 counter) {
|
||||
auto result = QString("update-%1-%2-%3-%4"
|
||||
).arg(V2Os
|
||||
).arg(V2Arch
|
||||
).arg(QString::fromLatin1(Core::Updates::OsName(V2Target.os))
|
||||
).arg(QString::fromLatin1(Core::Updates::ArchName(V2Target.arch))
|
||||
).arg(V2ChannelTag(channel)
|
||||
).arg(base);
|
||||
if (counter) {
|
||||
@@ -239,8 +238,47 @@ struct V2Keys {
|
||||
return V2Keys{ rootPem, std::move(*parsed) };
|
||||
}
|
||||
|
||||
// The build-time expiry watchdog: the client never rejects an expired
|
||||
// manifest (revocation is the kill switch), so this is the only place
|
||||
// the approaching dates are surfaced, ahead of the moment a channel key
|
||||
// expires and packing starts failing verification.
|
||||
void ReportExpiry(const Core::Updates::Manifest &manifest, Channel channel) {
|
||||
constexpr auto kWarnDays = 90;
|
||||
const auto now = QDateTime::currentSecsSinceEpoch();
|
||||
const auto report = [&](const QByteArray &what, qint64 expires) {
|
||||
if (!expires) {
|
||||
return;
|
||||
}
|
||||
const auto days = (expires - now) / (24 * 60 * 60);
|
||||
if (days < 0) {
|
||||
cout << "WARNING: " << what.constData() << " expired "
|
||||
<< -days << " days ago!\n";
|
||||
} else if (days < kWarnDays) {
|
||||
cout << "WARNING: " << what.constData() << " expires in "
|
||||
<< days << " days!\n";
|
||||
} else {
|
||||
cout << what.constData() << " expires in " << days << " days.\n";
|
||||
}
|
||||
};
|
||||
report("The manifest", manifest.expires);
|
||||
const auto i = manifest.channels.find(Core::Updates::ChannelName(channel));
|
||||
if (i == manifest.channels.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &group : i->second) {
|
||||
for (const auto &id : group) {
|
||||
for (const auto &key : manifest.keys) {
|
||||
if (key.id == id) {
|
||||
report("Key '" + id + "'", key.expires);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] QByteArray BuildV2Envelope(
|
||||
Channel channel,
|
||||
Core::Updates::Target target,
|
||||
quint64 version,
|
||||
qint64 created,
|
||||
const QByteArray &manifest,
|
||||
@@ -251,6 +289,8 @@ struct V2Keys {
|
||||
result.append(Core::Updates::kEnvelopeMagic, 4);
|
||||
AppendLeU32(result, Core::Updates::kEnvelopeFormat);
|
||||
result.append(char(uchar(channel)));
|
||||
result.append(char(uchar(target.os)));
|
||||
result.append(char(uchar(target.arch)));
|
||||
AppendLeU64(result, version);
|
||||
AppendLeU64(result, quint64(created));
|
||||
AppendLeU32(result, quint32(manifest.size()));
|
||||
@@ -313,12 +353,14 @@ struct V2Keys {
|
||||
[[nodiscard]] bool VerifyPackedV2(
|
||||
const QByteArray &fileBytes,
|
||||
const V2Keys &keys,
|
||||
Channel channel) {
|
||||
Channel channel,
|
||||
Core::Updates::Target target) {
|
||||
auto error = QString();
|
||||
const auto verified = Core::Updates::VerifyUpdate(
|
||||
fileBytes,
|
||||
channel,
|
||||
true, // betaSet
|
||||
target,
|
||||
0, // runningVersion
|
||||
keys.manifest,
|
||||
keys.rootPublicKeyPem,
|
||||
@@ -350,6 +392,7 @@ int WriteV2Update(const QByteArray &payload, quint32 baseVersion) {
|
||||
return -1;
|
||||
}
|
||||
const auto channel = *V2Channel;
|
||||
ReportExpiry(keys->manifest, channel);
|
||||
const auto version = Core::Updates::MakeUpdateVersion(
|
||||
baseVersion,
|
||||
V2Counter);
|
||||
@@ -358,6 +401,7 @@ int WriteV2Update(const QByteArray &payload, quint32 baseVersion) {
|
||||
auto signatures = std::vector<std::pair<QByteArray, QByteArray>>();
|
||||
const auto unsigned_ = BuildV2Envelope(
|
||||
channel,
|
||||
V2Target,
|
||||
version,
|
||||
QDateTime::currentSecsSinceEpoch(),
|
||||
keys->manifest.bytes,
|
||||
@@ -400,13 +444,14 @@ int WriteV2Update(const QByteArray &payload, quint32 baseVersion) {
|
||||
|
||||
const auto result = BuildV2Envelope(
|
||||
channel,
|
||||
V2Target,
|
||||
version,
|
||||
envelope->created,
|
||||
keys->manifest.bytes,
|
||||
keys->manifest.signature,
|
||||
signatures,
|
||||
payload);
|
||||
if (!VerifyPackedV2(result, *keys, channel)
|
||||
if (!VerifyPackedV2(result, *keys, channel, V2Target)
|
||||
|| !WriteWholeFile(name, result)) {
|
||||
return -1;
|
||||
}
|
||||
@@ -444,6 +489,7 @@ int EmbedV2Signatures() {
|
||||
cout << "The -channel param does not match the unsigned update!\n";
|
||||
return -1;
|
||||
}
|
||||
ReportExpiry(keys->manifest, envelope->channel);
|
||||
|
||||
auto signatures = std::vector<std::pair<QByteArray, QByteArray>>();
|
||||
for (const auto &[keyId, file] : V2EmbedSignatures) {
|
||||
@@ -483,6 +529,7 @@ int EmbedV2Signatures() {
|
||||
|
||||
const auto result = BuildV2Envelope(
|
||||
envelope->channel,
|
||||
envelope->target,
|
||||
envelope->version,
|
||||
envelope->created,
|
||||
envelope->manifest,
|
||||
@@ -491,7 +538,7 @@ int EmbedV2Signatures() {
|
||||
envelope->payload);
|
||||
const auto name = V2UnsignedFile.left(
|
||||
V2UnsignedFile.size() - int(strlen(".unsigned")));
|
||||
if (!VerifyPackedV2(result, *keys, envelope->channel)
|
||||
if (!VerifyPackedV2(result, *keys, envelope->channel, envelope->target)
|
||||
|| !WriteWholeFile(name, result)) {
|
||||
return -1;
|
||||
}
|
||||
@@ -603,20 +650,24 @@ int main(int argc, char *argv[])
|
||||
return writeAlphaKey();
|
||||
}
|
||||
|
||||
{
|
||||
using Core::Updates::Os;
|
||||
using Core::Updates::Arch;
|
||||
#ifdef Q_OS_WIN
|
||||
V2Os = QString("win");
|
||||
V2Arch = targetwinarm
|
||||
? QString("arm")
|
||||
: targetwin64
|
||||
? QString("x64")
|
||||
: QString("x86");
|
||||
V2Target.os = Os::Windows;
|
||||
V2Target.arch = targetwinarm
|
||||
? Arch::Arm
|
||||
: targetwin64
|
||||
? Arch::X64
|
||||
: Arch::X86;
|
||||
#elif defined Q_OS_MAC
|
||||
V2Os = QString("mac");
|
||||
V2Arch = targetarmac ? QString("arm") : QString("x64");
|
||||
V2Target.os = Os::Mac;
|
||||
V2Target.arch = targetarmac ? Arch::Arm : Arch::X64;
|
||||
#else
|
||||
V2Os = QString("linux");
|
||||
V2Arch = QString("x64");
|
||||
V2Target.os = Os::Linux;
|
||||
V2Target.arch = Arch::X64;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!V2UnsignedFile.isEmpty()) {
|
||||
return EmbedV2Signatures();
|
||||
@@ -632,6 +683,9 @@ int main(int argc, char *argv[])
|
||||
} else if (canary != (V2Counter > 0)) {
|
||||
cout << "Canary channels require a positive -counter, others require none!\n";
|
||||
return -1;
|
||||
} else if (!V2EmbedSignatures.empty()) {
|
||||
cout << "The -embed-signatures param requires -unsigned!\n";
|
||||
return -1;
|
||||
} else if (V2SigningInputFile.isEmpty()
|
||||
&& (V2LocalKeyFile.isEmpty() || V2LocalKeyId.isEmpty())) {
|
||||
cout << "Either -emit-signing-input or -local-key with -local-key-id is required!\n";
|
||||
@@ -859,14 +913,16 @@ int main(int argc, char *argv[])
|
||||
stream.next_out = (uint8_t*)resultCheck.data();
|
||||
|
||||
res = lzma_code(&stream, LZMA_FINISH);
|
||||
if (stream.avail_in) {
|
||||
cout << "Error in decompression, " << stream.avail_in << " bytes left in _in of " << compressedLen << " whole.\n";
|
||||
const auto availIn = stream.avail_in;
|
||||
const auto availOut = stream.avail_out;
|
||||
lzma_end(&stream);
|
||||
if (availIn) {
|
||||
cout << "Error in decompression, " << availIn << " bytes left in _in of " << compressedLen << " whole.\n";
|
||||
return -1;
|
||||
} else if (stream.avail_out) {
|
||||
cout << "Error in decompression, " << stream.avail_out << " bytes free left in _out of " << resultLen << " whole.\n";
|
||||
} else if (availOut) {
|
||||
cout << "Error in decompression, " << availOut << " bytes free left in _out of " << resultLen << " whole.\n";
|
||||
return -1;
|
||||
}
|
||||
lzma_end(&stream);
|
||||
if (res != LZMA_OK && res != LZMA_STREAM_END) {
|
||||
const char *msg;
|
||||
switch (res) {
|
||||
|
||||
@@ -252,6 +252,7 @@ private:
|
||||
void gotCanaryMessage(
|
||||
const MTPInputChannel &channel,
|
||||
const MTPmessages_Messages &result,
|
||||
int messageId,
|
||||
bool fallbackToPinned);
|
||||
void parseCanaryMetadata(
|
||||
const MTPInputChannel &channel,
|
||||
@@ -595,6 +596,16 @@ QString ExtractFilename(const QString &url) {
|
||||
[[nodiscard]] bool UnpackUpdateV2(
|
||||
const QString &filepath,
|
||||
const QByteArray &content) {
|
||||
// The expected target follows the feed key, not the build: an x64
|
||||
// build under Rosetta asks for armac and must accept that package.
|
||||
const auto target = Updates::TargetFromPlatformKey(
|
||||
Platform::AutoUpdateKey().toLatin1());
|
||||
if (!target) {
|
||||
LOG(("Update Error: No v2 target for platform key '%1'."
|
||||
).arg(Platform::AutoUpdateKey()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The full verification happens before any decompression, so no
|
||||
// unauthenticated bytes ever reach the LZMA or QDataStream parsers.
|
||||
auto error = QString();
|
||||
@@ -602,6 +613,7 @@ QString ExtractFilename(const QString &url) {
|
||||
content,
|
||||
BuildUpdateChannel,
|
||||
AppBetaVersion || cInstallBetaVersion(),
|
||||
*target,
|
||||
RunningUpdateVersion(),
|
||||
HeldManifest(),
|
||||
Updates::RootPublicKeyPem(),
|
||||
@@ -690,6 +702,9 @@ bool UnpackUpdate(const QString &filepath) {
|
||||
if (!input.open(QIODevice::ReadOnly)) {
|
||||
LOG(("Update Error: cant read updates file!"));
|
||||
return false;
|
||||
} else if (input.size() > Loader::kMaxFileSize) {
|
||||
LOG(("Update Error: updates file is too large: %1").arg(input.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined Q_OS_WIN && !defined TDESKTOP_USE_PACKAGED // use Lzma SDK for win
|
||||
@@ -703,6 +718,12 @@ bool UnpackUpdate(const QString &filepath) {
|
||||
|
||||
if (Updates::IsV2UpdateFile(compressed)) {
|
||||
return UnpackUpdateV2(filepath, compressed);
|
||||
} else if (BuildIsCanary) {
|
||||
// The channel policy lives in the v2 envelope only, a classical
|
||||
// RSA package has no channel and would let any official v1 file
|
||||
// posted to the canary channel jump a canary off its lane.
|
||||
LOG(("Update Error: canary builds accept only v2 updates."));
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 compressedLen = compressed.size() - hSize;
|
||||
@@ -1182,7 +1203,7 @@ void HttpLoaderActor::gotMetaData() {
|
||||
if (QString::fromUtf8(pair.first).toLower() == "content-range") {
|
||||
const auto m = QRegularExpression(u"/(\\d+)([^\\d]|$)"_q).match(QString::fromUtf8(pair.second));
|
||||
if (m.hasMatch()) {
|
||||
_parent->writeChunk({}, m.captured(1).toInt());
|
||||
_parent->writeChunk({}, m.captured(1).toLongLong());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1405,7 +1426,7 @@ void MtpChecker::requestCanaryMetadata(
|
||||
1,
|
||||
MTP_inputMessageID(MTP_int(messageId)))),
|
||||
[=](const MTPmessages_Messages &result) {
|
||||
gotCanaryMessage(channel, result, fallbackToPinned);
|
||||
gotCanaryMessage(channel, result, messageId, fallbackToPinned);
|
||||
},
|
||||
failHandler());
|
||||
}
|
||||
@@ -1433,8 +1454,9 @@ void MtpChecker::requestCanaryPinnedFallback(const MTPInputChannel &channel) {
|
||||
void MtpChecker::gotCanaryMessage(
|
||||
const MTPInputChannel &channel,
|
||||
const MTPmessages_Messages &result,
|
||||
int messageId,
|
||||
bool fallbackToPinned) {
|
||||
const auto message = MTP::GetMessagesElement(result);
|
||||
const auto message = MTP::GetMessagesElement(result, messageId);
|
||||
if (!message || message->type() != mtpc_message) {
|
||||
if (fallbackToPinned) {
|
||||
requestCanaryPinnedFallback(channel);
|
||||
@@ -1459,6 +1481,11 @@ void MtpChecker::parseCanaryMetadata(
|
||||
return;
|
||||
}
|
||||
const auto object = document.object();
|
||||
if (object.value(u"format"_q).toDouble() != 1.) {
|
||||
LOG(("Update Error: Unknown canary metadata format."));
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto decode = [](const QJsonValue &value) {
|
||||
if (!value.isString()) {
|
||||
@@ -1472,11 +1499,15 @@ void MtpChecker::parseCanaryMetadata(
|
||||
const auto manifest = decode(object.value(u"manifest"_q));
|
||||
const auto manifestSig = decode(object.value(u"manifest_sig"_q));
|
||||
if (!manifest.isEmpty() && !manifestSig.isEmpty()) {
|
||||
auto error = QString();
|
||||
if (auto parsed = Updates::ParseVerifiedManifest(
|
||||
manifest,
|
||||
manifestSig,
|
||||
Updates::RootPublicKeyPem())) {
|
||||
Updates::RootPublicKeyPem(),
|
||||
&error)) {
|
||||
AdoptManifest(*parsed);
|
||||
} else {
|
||||
LOG(("Update Error: Bad canary metadata manifest: %1").arg(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1484,35 +1515,46 @@ void MtpChecker::parseCanaryMetadata(
|
||||
const auto platform = Platform::AutoUpdateKey();
|
||||
auto bestVersion = quint64(0);
|
||||
auto bestPostId = 0;
|
||||
const auto readU32 = [](const QJsonValue &value, quint32 *result) {
|
||||
const auto number = value.toDouble();
|
||||
if (!(number >= 0.) || !(number <= 4294967295.)) {
|
||||
return false;
|
||||
}
|
||||
*result = quint32(number);
|
||||
return (double(*result) == number);
|
||||
};
|
||||
const auto consider = [&](const QByteArray &name) {
|
||||
const auto entry = channels.value(QLatin1String(name)).toObject();
|
||||
if (entry.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto base = entry.value(u"base"_q).toDouble();
|
||||
const auto counter = entry.value(u"counter"_q).toDouble();
|
||||
if (base <= 0
|
||||
|| base != double(quint32(base))
|
||||
|| counter < 0
|
||||
|| counter != double(quint32(counter))) {
|
||||
auto base = quint32(0);
|
||||
auto counter = quint32(0);
|
||||
auto postId = quint32(0);
|
||||
if (!readU32(entry.value(u"base"_q), &base)
|
||||
|| !base
|
||||
|| !readU32(entry.value(u"counter"_q), &counter)
|
||||
|| !readU32(
|
||||
entry.value(u"posts"_q).toObject().value(platform),
|
||||
&postId)
|
||||
|| !postId
|
||||
|| postId > quint32(0x7FFFFFFF)) {
|
||||
return;
|
||||
}
|
||||
const auto version = Updates::MakeUpdateVersion(
|
||||
quint32(base),
|
||||
quint32(counter));
|
||||
const auto postId = int(base::SafeRound(
|
||||
entry.value(u"posts"_q).toObject().value(platform).toDouble()));
|
||||
if (version > bestVersion && postId > 0) {
|
||||
const auto version = Updates::MakeUpdateVersion(base, counter);
|
||||
if (version > bestVersion) {
|
||||
bestVersion = version;
|
||||
bestPostId = postId;
|
||||
bestPostId = int(postId);
|
||||
}
|
||||
};
|
||||
consider(Updates::ChannelName(BuildUpdateChannel));
|
||||
if (BuildUpdateChannel == Updates::Channel::CanaryPublic) {
|
||||
// Dormancy rescue: a stale public canary channel may point to a
|
||||
// newer stable release. Package verification still enforces the
|
||||
// strictly-greater-base channel policy on whatever is downloaded.
|
||||
// newer stable or beta release. Package verification still
|
||||
// enforces the strictly-greater-base channel policy on whatever
|
||||
// is downloaded.
|
||||
consider(Updates::ChannelName(Updates::Channel::Stable));
|
||||
consider(Updates::ChannelName(Updates::Channel::Beta));
|
||||
}
|
||||
if (!bestVersion || bestVersion <= RunningUpdateVersion()) {
|
||||
done(nullptr);
|
||||
@@ -2235,6 +2277,10 @@ bool checkReadyUpdate() {
|
||||
ClearAll();
|
||||
return false;
|
||||
}
|
||||
} else if (BuildUpdateChannel == Updates::Channel::CanaryPrivate) {
|
||||
LOG(("Update Error: cant install a non-canary version %1 on a private canary").arg(versionNum));
|
||||
ClearAll();
|
||||
return false;
|
||||
} else if (versionNum <= AppVersion) {
|
||||
LOG(("Update Error: cant install version %1 having version %2").arg(versionNum).arg(AppVersion));
|
||||
ClearAll();
|
||||
|
||||
@@ -112,6 +112,9 @@ void SetError(QString *error, const QString &text) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto number = value.toDouble();
|
||||
if (!(number > -9007199254740992.) || !(number < 9007199254740992.)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto result = qint64(number);
|
||||
if (double(result) != number) {
|
||||
return std::nullopt;
|
||||
@@ -455,7 +458,7 @@ struct Reader {
|
||||
}
|
||||
auto result = QByteArray(
|
||||
reinterpret_cast<const char*>(data + offset),
|
||||
int(count));
|
||||
qsizetype(count));
|
||||
offset += count;
|
||||
return result;
|
||||
}
|
||||
@@ -486,6 +489,41 @@ std::optional<Channel> ChannelFromName(const QByteArray &name) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QByteArray OsName(Os os) {
|
||||
switch (os) {
|
||||
case Os::Windows: return QByteArrayLiteral("win");
|
||||
case Os::Mac: return QByteArrayLiteral("mac");
|
||||
case Os::Linux: return QByteArrayLiteral("linux");
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QByteArray ArchName(Arch arch) {
|
||||
switch (arch) {
|
||||
case Arch::X86: return QByteArrayLiteral("x86");
|
||||
case Arch::X64: return QByteArrayLiteral("x64");
|
||||
case Arch::Arm: return QByteArrayLiteral("arm");
|
||||
}
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
std::optional<Target> TargetFromPlatformKey(const QByteArray &key) {
|
||||
if (key == "win") {
|
||||
return Target{ Os::Windows, Arch::X86 };
|
||||
} else if (key == "win64") {
|
||||
return Target{ Os::Windows, Arch::X64 };
|
||||
} else if (key == "winarm") {
|
||||
return Target{ Os::Windows, Arch::Arm };
|
||||
} else if (key == "mac") {
|
||||
return Target{ Os::Mac, Arch::X64 };
|
||||
} else if (key == "armac") {
|
||||
return Target{ Os::Mac, Arch::Arm };
|
||||
} else if (key == "linux") {
|
||||
return Target{ Os::Linux, Arch::X64 };
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Manifest> ParseVerifiedManifest(
|
||||
const QByteArray &json,
|
||||
const QByteArray &signature,
|
||||
@@ -596,6 +634,18 @@ std::optional<Envelope> ParseEnvelope(
|
||||
}
|
||||
result.channel = Channel(*channel);
|
||||
|
||||
const auto os = reader.readU8();
|
||||
if (!os || *os > quint8(Os::Linux)) {
|
||||
SetError(error, QStringLiteral("Bad envelope os."));
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto arch = reader.readU8();
|
||||
if (!arch || *arch > quint8(Arch::Arm)) {
|
||||
SetError(error, QStringLiteral("Bad envelope arch."));
|
||||
return std::nullopt;
|
||||
}
|
||||
result.target = Target{ Os(*os), Arch(*arch) };
|
||||
|
||||
const auto version = reader.readU64();
|
||||
if (!version) {
|
||||
SetError(error, QStringLiteral("Bad envelope version."));
|
||||
@@ -672,7 +722,9 @@ std::optional<Envelope> ParseEnvelope(
|
||||
}
|
||||
|
||||
const auto payloadLength = reader.readU32();
|
||||
if (!payloadLength || !*payloadLength) {
|
||||
if (!payloadLength
|
||||
|| !*payloadLength
|
||||
|| *payloadLength > kMaxPayloadSize) {
|
||||
SetError(error, QStringLiteral("Bad envelope payload size."));
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -805,6 +857,7 @@ std::optional<VerifiedUpdate> VerifyUpdate(
|
||||
const QByteArray &data,
|
||||
Channel buildChannel,
|
||||
bool betaSet,
|
||||
Target expectedTarget,
|
||||
quint64 runningVersion,
|
||||
const std::optional<Manifest> &held,
|
||||
const QByteArray &rootPublicKeyPem,
|
||||
@@ -813,6 +866,11 @@ std::optional<VerifiedUpdate> VerifyUpdate(
|
||||
auto envelope = ParseEnvelope(data, error);
|
||||
if (!envelope) {
|
||||
return std::nullopt;
|
||||
} else if (!(envelope->target == expectedTarget)) {
|
||||
SetError(
|
||||
error,
|
||||
QStringLiteral("Package target is not this platform."));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto carried = ParseVerifiedManifest(
|
||||
|
||||
@@ -34,6 +34,44 @@ enum class Channel : uchar {
|
||||
[[nodiscard]] QByteArray ChannelName(Channel channel);
|
||||
[[nodiscard]] std::optional<Channel> ChannelFromName(const QByteArray &name);
|
||||
|
||||
enum class Os : uchar {
|
||||
Windows = 0,
|
||||
Mac = 1,
|
||||
Linux = 2,
|
||||
};
|
||||
|
||||
enum class Arch : uchar {
|
||||
X86 = 0,
|
||||
X64 = 1,
|
||||
Arm = 2,
|
||||
};
|
||||
|
||||
// The target a package was built for, part of the signed region: a valid
|
||||
// signature for one arch can't be re-routed to clients of another one.
|
||||
struct Target {
|
||||
Os os = Os::Windows;
|
||||
Arch arch = Arch::X86;
|
||||
|
||||
friend inline bool operator==(Target a, Target b) {
|
||||
return (a.os == b.os) && (a.arch == b.arch);
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] QByteArray OsName(Os os);
|
||||
[[nodiscard]] QByteArray ArchName(Arch arch);
|
||||
|
||||
// The feed key of Platform::AutoUpdateKey(): win / win64 / winarm / mac
|
||||
// / armac / linux. It names the package a client should RECEIVE, which is
|
||||
// not always the client's own build (an x64 build under Rosetta asks for
|
||||
// armac), so clients compute the expected target from it, never from
|
||||
// compile-time macros.
|
||||
[[nodiscard]] std::optional<Target> TargetFromPlatformKey(
|
||||
const QByteArray &key);
|
||||
|
||||
// The payload is the only envelope field without a tight structural cap,
|
||||
// this one matches the updater download limit.
|
||||
inline constexpr auto kMaxPayloadSize = quint32(256 * 1024 * 1024);
|
||||
|
||||
[[nodiscard]] constexpr quint64 MakeUpdateVersion(
|
||||
quint32 base,
|
||||
quint32 counter) {
|
||||
@@ -86,6 +124,7 @@ struct EnvelopeSignature {
|
||||
|
||||
struct Envelope {
|
||||
Channel channel = Channel::Stable;
|
||||
Target target;
|
||||
quint64 version = 0;
|
||||
qint64 created = 0;
|
||||
QByteArray manifest;
|
||||
@@ -153,6 +192,7 @@ struct VerifiedUpdate {
|
||||
const QByteArray &data,
|
||||
Channel buildChannel,
|
||||
bool betaSet,
|
||||
Target expectedTarget,
|
||||
quint64 runningVersion,
|
||||
const std::optional<Manifest> &held,
|
||||
const QByteArray &rootPublicKeyPem,
|
||||
|
||||
@@ -36,8 +36,9 @@ std::optional<MTPInputChannel> ExtractChannel(
|
||||
}
|
||||
|
||||
std::optional<DedicatedLoader::File> ParseFile(
|
||||
const MTPmessages_Messages &result) {
|
||||
const auto message = GetMessagesElement(result);
|
||||
const MTPmessages_Messages &result,
|
||||
int postId) {
|
||||
const auto message = GetMessagesElement(result, postId);
|
||||
if (!message || message->type() != mtpc_message) {
|
||||
LOG(("Update Error: MTP file message not found."));
|
||||
return std::nullopt;
|
||||
@@ -69,8 +70,8 @@ std::optional<DedicatedLoader::File> ParseFile(
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto size = int64(fields.vsize().v);
|
||||
if (size <= 0) {
|
||||
LOG(("Update Error: MTP file size is invalid."));
|
||||
if (size <= 0 || size > AbstractDedicatedLoader::kMaxFileSize) {
|
||||
LOG(("Update Error: MTP file size is invalid: %1.").arg(size));
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto location = MTP_inputDocumentFileLocation(
|
||||
@@ -267,8 +268,15 @@ void AbstractDedicatedLoader::threadSafeFailed() {
|
||||
});
|
||||
}
|
||||
|
||||
void AbstractDedicatedLoader::writeChunk(bytes::const_span data, int totalSize) {
|
||||
const auto size = data.size();
|
||||
void AbstractDedicatedLoader::writeChunk(bytes::const_span data, int64 totalSize) {
|
||||
const auto size = int64(data.size());
|
||||
if (totalSize > kMaxFileSize || alreadySize() + size > kMaxFileSize) {
|
||||
LOG(("Update Error: Download exceeds the size limit: %1 / %2."
|
||||
).arg(alreadySize() + size
|
||||
).arg(totalSize));
|
||||
threadSafeFailed();
|
||||
return;
|
||||
}
|
||||
if (size > 0) {
|
||||
const auto written = _output.write(QByteArray::fromRawData(
|
||||
reinterpret_cast<const char*>(data.data()),
|
||||
@@ -442,13 +450,20 @@ void ResolveChannel(
|
||||
}
|
||||
|
||||
std::optional<MTPMessage> GetMessagesElement(
|
||||
const MTPmessages_Messages &list) {
|
||||
const MTPmessages_Messages &list,
|
||||
int messageId) {
|
||||
return list.match([&](const MTPDmessages_messagesNotModified &) {
|
||||
return std::optional<MTPMessage>(std::nullopt);
|
||||
}, [&](const auto &data) {
|
||||
return data.vmessages().v.isEmpty()
|
||||
? std::nullopt
|
||||
: std::make_optional(data.vmessages().v[0]);
|
||||
}, [&](const auto &data) -> std::optional<MTPMessage> {
|
||||
for (const auto &message : data.vmessages().v) {
|
||||
const auto id = message.match([](const auto &data) {
|
||||
return data.vid().v;
|
||||
});
|
||||
if (!messageId || id == messageId) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -457,8 +472,9 @@ void StartDedicatedLoader(
|
||||
const DedicatedLoader::Location &location,
|
||||
const QString &folder,
|
||||
Fn<void(std::unique_ptr<DedicatedLoader>)> ready) {
|
||||
const auto postId = location.postId;
|
||||
const auto doneHandler = [=](const MTPmessages_Messages &result) {
|
||||
const auto file = ParseFile(result);
|
||||
const auto file = ParseFile(result, postId);
|
||||
ready(file
|
||||
? std::make_unique<MTP::DedicatedLoader>(
|
||||
mtp->session(),
|
||||
@@ -472,7 +488,6 @@ void StartDedicatedLoader(
|
||||
ready(nullptr);
|
||||
};
|
||||
|
||||
const auto postId = location.postId;
|
||||
const auto request = [=](const MTPInputChannel &channel) {
|
||||
mtp->send(
|
||||
MTPchannels_GetMessages(
|
||||
|
||||
@@ -87,7 +87,7 @@ protected:
|
||||
void threadSafeReady();
|
||||
|
||||
// Single threaded.
|
||||
void writeChunk(bytes::const_span data, int totalSize);
|
||||
void writeChunk(bytes::const_span data, int64 totalSize);
|
||||
|
||||
private:
|
||||
virtual void startLoading() = 0;
|
||||
@@ -162,8 +162,11 @@ void ResolveChannel(
|
||||
Fn<void(const MTPInputChannel &channel)> done,
|
||||
Fn<void()> fail);
|
||||
|
||||
// With a non-zero messageId only the message with that exact id counts,
|
||||
// the server may answer a getMessages request with a different message.
|
||||
std::optional<MTPMessage> GetMessagesElement(
|
||||
const MTPmessages_Messages &list);
|
||||
const MTPmessages_Messages &list,
|
||||
int messageId = 0);
|
||||
|
||||
void StartDedicatedLoader(
|
||||
not_null<MTP::WeakInstance*> mtp,
|
||||
|
||||
@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
#include "lang/lang_instance.h"
|
||||
|
||||
#include <QtCore/QDirIterator>
|
||||
#include <QtCore/QSaveFile>
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
#include <unistd.h>
|
||||
@@ -603,10 +604,12 @@ void writeUpdateManifest(
|
||||
|| manifest.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QFile f(updateManifestFile());
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.write(signature);
|
||||
f.write(manifest);
|
||||
QSaveFile f(updateManifestFile());
|
||||
if (!f.open(QIODevice::WriteOnly)
|
||||
|| f.write(signature) != signature.size()
|
||||
|| f.write(manifest) != manifest.size()
|
||||
|| !f.commit()) {
|
||||
LOG(("Storage Error: Could not write the update manifest."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
// Focused console tests for the v2 update verification: throwaway keys are
|
||||
// generated in-process with OpenSSL, so no fixture files and no network.
|
||||
|
||||
#include "core/update_keys.h"
|
||||
#include "core/update_verify.h"
|
||||
|
||||
#include <QtCore/QJsonArray>
|
||||
@@ -35,6 +36,10 @@ using namespace Core::Updates;
|
||||
int FailedChecks = 0;
|
||||
int TotalChecks = 0;
|
||||
|
||||
constexpr auto kTarget = Target{ Os::Linux, Arch::X64 };
|
||||
constexpr auto kOtherArch = Target{ Os::Linux, Arch::Arm };
|
||||
constexpr auto kOtherOs = Target{ Os::Mac, Arch::X64 };
|
||||
|
||||
void Check(bool condition, const char *name) {
|
||||
++TotalChecks;
|
||||
if (!condition) {
|
||||
@@ -283,11 +288,14 @@ void AppendLeU64(QByteArray &to, quint64 value) {
|
||||
const QByteArray &manifestSig,
|
||||
const std::vector<std::pair<QByteArray, QByteArray>> &signatures,
|
||||
const QByteArray &payload,
|
||||
Target target = kTarget,
|
||||
quint32 format = kEnvelopeFormat) {
|
||||
auto result = QByteArray();
|
||||
result.append(kEnvelopeMagic, 4);
|
||||
AppendLeU32(result, format);
|
||||
result.append(char(uchar(channel)));
|
||||
result.append(char(uchar(target.os)));
|
||||
result.append(char(uchar(target.arch)));
|
||||
AppendLeU64(result, version);
|
||||
AppendLeU64(result, quint64(1700000000));
|
||||
AppendLeU32(result, quint32(manifest.size()));
|
||||
@@ -312,14 +320,16 @@ void AppendLeU64(QByteArray &to, quint64 value) {
|
||||
const QByteArray &manifest,
|
||||
const QByteArray &manifestSig,
|
||||
const std::vector<const TestKey*> &signers,
|
||||
const QByteArray &payload) {
|
||||
const QByteArray &payload,
|
||||
Target target = kTarget) {
|
||||
const auto unsignedBytes = BuildEnvelope(
|
||||
channel,
|
||||
version,
|
||||
manifest,
|
||||
manifestSig,
|
||||
{},
|
||||
payload);
|
||||
payload,
|
||||
target);
|
||||
const auto envelope = ParseEnvelope(unsignedBytes);
|
||||
if (!envelope) {
|
||||
return QByteArray();
|
||||
@@ -335,7 +345,8 @@ void AppendLeU64(QByteArray &to, quint64 value) {
|
||||
manifest,
|
||||
manifestSig,
|
||||
signatures,
|
||||
payload);
|
||||
payload,
|
||||
target);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -399,6 +410,7 @@ int main(int argc, char *argv[]) {
|
||||
data,
|
||||
build,
|
||||
betaSet,
|
||||
kTarget,
|
||||
running,
|
||||
held,
|
||||
rootPem,
|
||||
@@ -406,6 +418,21 @@ int main(int argc, char *argv[]) {
|
||||
error);
|
||||
};
|
||||
|
||||
{ // The committed trust files must verify with the pinned root.
|
||||
auto error = QString();
|
||||
const auto embedded = ParseVerifiedManifest(
|
||||
EmbeddedManifest(),
|
||||
EmbeddedManifestSignature(),
|
||||
RootPublicKeyPem(),
|
||||
&error);
|
||||
Check(embedded.has_value(), "embedded manifest verifies");
|
||||
Check(embedded && embedded->version >= 1, "embedded manifest version");
|
||||
Check(embedded && embedded->channels.size() == 4,
|
||||
"embedded manifest lists all four channels");
|
||||
Check(embedded && !embedded->keys.empty(),
|
||||
"embedded manifest has usable keys");
|
||||
}
|
||||
|
||||
{ // A good stable package needs one rl AND one rc signature.
|
||||
const auto good = BuildSignedEnvelope(
|
||||
Channel::Stable,
|
||||
@@ -594,6 +621,7 @@ int main(int argc, char *argv[]) {
|
||||
byExpired,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kTarget,
|
||||
runningCanary,
|
||||
heldSame,
|
||||
rootPem,
|
||||
@@ -603,6 +631,7 @@ int main(int argc, char *argv[]) {
|
||||
byExpired,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kTarget,
|
||||
runningCanary,
|
||||
heldSame,
|
||||
rootPem,
|
||||
@@ -640,6 +669,7 @@ int main(int argc, char *argv[]) {
|
||||
oldManifestPackage,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kTarget,
|
||||
runningCanary,
|
||||
heldRevoking,
|
||||
rootPem,
|
||||
@@ -674,6 +704,7 @@ int main(int argc, char *argv[]) {
|
||||
package,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kTarget,
|
||||
runningCanary,
|
||||
std::nullopt,
|
||||
rootPem,
|
||||
@@ -744,9 +775,17 @@ int main(int argc, char *argv[]) {
|
||||
manifestSig,
|
||||
{},
|
||||
payload,
|
||||
kTarget,
|
||||
3);
|
||||
Check(!ParseEnvelope(badFormat), "unknown format rejected");
|
||||
|
||||
auto badOs = good;
|
||||
badOs[9] = char(3);
|
||||
Check(!ParseEnvelope(badOs), "unknown os rejected");
|
||||
auto badArch = good;
|
||||
badArch[10] = char(3);
|
||||
Check(!ParseEnvelope(badArch), "unknown arch rejected");
|
||||
|
||||
auto badMagic = good;
|
||||
badMagic[0] = 'X';
|
||||
Check(!IsV2UpdateFile(badMagic), "bad magic not detected as v2");
|
||||
@@ -791,6 +830,96 @@ int main(int argc, char *argv[]) {
|
||||
Check(verify(withExtra, Channel::CanaryPublic, false, runningCanary)
|
||||
.has_value(),
|
||||
"unknown extra signature entries are ignored");
|
||||
|
||||
// A DER-encoded ECDSA signature is not the raw r||s the envelope
|
||||
// stores, the fixed 64-byte check must refuse it.
|
||||
auto der = QByteArray();
|
||||
der.append(char(0x30)).append(char(0x44));
|
||||
der.append(char(0x02)).append(char(0x20)).append(QByteArray(32, 'r'));
|
||||
der.append(char(0x02)).append(char(0x20)).append(QByteArray(32, 's'));
|
||||
const auto withDer = BuildEnvelope(
|
||||
Channel::CanaryPublic,
|
||||
MakeUpdateVersion(5000000, 41),
|
||||
manifestJson,
|
||||
manifestSig,
|
||||
{ { cp.id, der } },
|
||||
payload);
|
||||
Check(!verify(withDer, Channel::CanaryPublic, false, runningCanary),
|
||||
"DER-encoded ES256 signature rejected");
|
||||
}
|
||||
|
||||
{ // The signed target must be the one this client should receive.
|
||||
const auto otherArch = BuildSignedEnvelope(
|
||||
Channel::CanaryPublic,
|
||||
MakeUpdateVersion(5000000, 41),
|
||||
manifestJson,
|
||||
manifestSig,
|
||||
{ &cp },
|
||||
payload,
|
||||
kOtherArch);
|
||||
Check(!verify(otherArch, Channel::CanaryPublic, false, runningCanary),
|
||||
"validly signed package for another arch rejected");
|
||||
Check(VerifyUpdate(
|
||||
otherArch,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kOtherArch,
|
||||
runningCanary,
|
||||
held,
|
||||
rootPem,
|
||||
kNow).has_value(),
|
||||
"the same package accepted by a client expecting that arch");
|
||||
|
||||
const auto otherOs = BuildSignedEnvelope(
|
||||
Channel::CanaryPublic,
|
||||
MakeUpdateVersion(5000000, 41),
|
||||
manifestJson,
|
||||
manifestSig,
|
||||
{ &cp },
|
||||
payload,
|
||||
kOtherOs);
|
||||
Check(!verify(otherOs, Channel::CanaryPublic, false, runningCanary),
|
||||
"validly signed package for another os rejected");
|
||||
|
||||
const auto good = BuildSignedEnvelope(
|
||||
Channel::CanaryPublic,
|
||||
MakeUpdateVersion(5000000, 41),
|
||||
manifestJson,
|
||||
manifestSig,
|
||||
{ &cp },
|
||||
payload);
|
||||
auto retargeted = good;
|
||||
retargeted[10] = char(uchar(Arch::Arm));
|
||||
Check(!VerifyUpdate(
|
||||
retargeted,
|
||||
Channel::CanaryPublic,
|
||||
false,
|
||||
kOtherArch,
|
||||
runningCanary,
|
||||
held,
|
||||
rootPem,
|
||||
kNow),
|
||||
"target byte tampering breaks the signature");
|
||||
|
||||
Check(TargetFromPlatformKey("armac")
|
||||
&& *TargetFromPlatformKey("armac") == Target{ Os::Mac, Arch::Arm }
|
||||
&& TargetFromPlatformKey("win64")
|
||||
&& *TargetFromPlatformKey("win64") == Target{ Os::Windows, Arch::X64 }
|
||||
&& !TargetFromPlatformKey("amiga"),
|
||||
"platform keys map to targets");
|
||||
}
|
||||
|
||||
{ // Dormancy rescue also works for beta packages on canary-public.
|
||||
const auto beta = BuildSignedEnvelope(
|
||||
Channel::Beta,
|
||||
MakeUpdateVersion(5000001, 0),
|
||||
manifestJson,
|
||||
manifestSig,
|
||||
{ &rl, &rc },
|
||||
payload);
|
||||
Check(verify(beta, Channel::CanaryPublic, false, runningCanary)
|
||||
.has_value(),
|
||||
"beta with greater base accepted on canary-public");
|
||||
}
|
||||
|
||||
std::cout << (TotalChecks - FailedChecks) << "/" << TotalChecks
|
||||
|
||||
@@ -202,7 +202,7 @@ if %AlphaVersion% neq 0 (
|
||||
echo Deploy folder for version %AppVersionStr% already exists!
|
||||
exit /b 1
|
||||
)
|
||||
if exist %ReleasePath%\tupdate%AppVersion% (
|
||||
if exist %ReleasePath%\%UpdateFile% (
|
||||
echo Update file for version %AppVersion% already exists!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ if ! openssl genpkey -algorithm ed25519 2>/dev/null >/dev/null; then
|
||||
fi
|
||||
|
||||
echo "Working in $WORKDIR"
|
||||
mkdir -p "$WORKDIR"
|
||||
cd "$WORKDIR"
|
||||
mkdir -p keys app
|
||||
|
||||
|
||||
@@ -35,24 +35,32 @@ def b64url_decode(text: str) -> bytes:
|
||||
|
||||
|
||||
def der_to_raw_rs(der: bytes) -> bytes:
|
||||
# Minimal DER parse of SEQUENCE { INTEGER r, INTEGER s }.
|
||||
# Minimal DER parse of SEQUENCE { INTEGER r, INTEGER s }, strict
|
||||
# about lengths: the input is normally trusted openssl output, but a
|
||||
# malformed signature must fail here rather than in the packer.
|
||||
def read_len(data, offset):
|
||||
first = data[offset]
|
||||
offset += 1
|
||||
if first < 0x80:
|
||||
return first, offset
|
||||
count = first & 0x7F
|
||||
if count == 0 or count > 2 or offset + count > len(data):
|
||||
raise ValueError('bad DER length')
|
||||
value = int.from_bytes(data[offset:offset + count], 'big')
|
||||
return value, offset + count
|
||||
|
||||
if not der or der[0] != 0x30:
|
||||
raise ValueError('not a DER SEQUENCE')
|
||||
_, offset = read_len(der, 1)
|
||||
total, offset = read_len(der, 1)
|
||||
if offset + total != len(der):
|
||||
raise ValueError('DER SEQUENCE length does not match the input')
|
||||
|
||||
def read_int(data, offset):
|
||||
if data[offset] != 0x02:
|
||||
if offset >= len(data) or data[offset] != 0x02:
|
||||
raise ValueError('not a DER INTEGER')
|
||||
length, offset = read_len(data, offset + 1)
|
||||
if length == 0 or offset + length > len(data):
|
||||
raise ValueError('bad DER INTEGER length')
|
||||
value = data[offset:offset + length].lstrip(b'\x00')
|
||||
if len(value) > 32:
|
||||
raise ValueError('integer too long for P-256')
|
||||
@@ -60,6 +68,8 @@ def der_to_raw_rs(der: bytes) -> bytes:
|
||||
|
||||
r, offset = read_int(der, offset)
|
||||
s, offset = read_int(der, offset)
|
||||
if offset != len(der):
|
||||
raise ValueError('trailing bytes after the DER signature')
|
||||
return r + s
|
||||
|
||||
|
||||
@@ -72,6 +82,7 @@ def sign_azure(digest: bytes, args) -> bytes:
|
||||
'--algorithm', 'ES256',
|
||||
'--digest', b64url(digest),
|
||||
'--output', 'json',
|
||||
'--only-show-errors',
|
||||
]
|
||||
if args.az_key_version:
|
||||
command += ['--version', args.az_key_version]
|
||||
|
||||
@@ -64,6 +64,20 @@ set(TDESKTOP_CANARY_PUBLIC_CHANNEL "" CACHE STRING "Public canary channel userna
|
||||
set(TDESKTOP_CANARY_PRIVATE_CHANNEL_ID "0" CACHE STRING "Private canary channel numeric id (canary-private builds).")
|
||||
set(TDESKTOP_CANARY_METADATA_MSG_ID "0" CACHE STRING "Fixed metadata message id in the canary channel.")
|
||||
|
||||
# CI passes these straight from repository variables, an unset variable
|
||||
# arrives as an empty string and must mean "not configured", not an
|
||||
# empty macro body.
|
||||
foreach(numeric_option
|
||||
TDESKTOP_CANARY_COUNTER
|
||||
TDESKTOP_CANARY_PRIVATE_CHANNEL_ID
|
||||
TDESKTOP_CANARY_METADATA_MSG_ID)
|
||||
if (${numeric_option} STREQUAL "")
|
||||
set(${numeric_option} 0)
|
||||
elseif (NOT ${numeric_option} MATCHES "^[0-9]+$")
|
||||
message(FATAL_ERROR "${numeric_option} must be a non-negative integer, got '${${numeric_option}}'.")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (TDESKTOP_UPDATE_CHANNEL STREQUAL "stable")
|
||||
set(tdesktop_update_channel_value 0)
|
||||
elseif (TDESKTOP_UPDATE_CHANNEL STREQUAL "beta")
|
||||
|
||||
37
Telegram/cmake/test_update_verify.cmake
Normal file
37
Telegram/cmake/test_update_verify.cmake
Normal file
@@ -0,0 +1,37 @@
|
||||
# This file is part of Telegram Desktop,
|
||||
# the official desktop application for the Telegram messaging service.
|
||||
#
|
||||
# For license and copyright information please follow this link:
|
||||
# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
# The focused v2 update verification test is built alongside the Packer
|
||||
# for special targets as well as with the development test apps: it runs
|
||||
# the exact translation unit that verifies updates in the client and it
|
||||
# checks the committed trust files against the pinned root.
|
||||
|
||||
add_executable(test_update_verify)
|
||||
init_target(test_update_verify "(tests)")
|
||||
|
||||
target_include_directories(test_update_verify PRIVATE ${src_loc})
|
||||
|
||||
nice_target_sources(test_update_verify ${src_loc}
|
||||
PRIVATE
|
||||
core/update_keys.cpp
|
||||
core/update_keys.h
|
||||
core/update_verify.cpp
|
||||
core/update_verify.h
|
||||
tests/test_update_verify.cpp
|
||||
)
|
||||
|
||||
target_include_directories(test_update_verify PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/gen)
|
||||
add_dependencies(test_update_verify Telegram_update_keys)
|
||||
|
||||
target_link_libraries(test_update_verify
|
||||
PRIVATE
|
||||
desktop-app::external_qt
|
||||
desktop-app::external_openssl
|
||||
)
|
||||
|
||||
set_target_properties(test_update_verify PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
add_dependencies(Telegram test_update_verify)
|
||||
@@ -4,28 +4,6 @@
|
||||
# For license and copyright information please follow this link:
|
||||
# https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
|
||||
add_executable(test_update_verify)
|
||||
init_target(test_update_verify "(tests)")
|
||||
|
||||
target_include_directories(test_update_verify PRIVATE ${src_loc})
|
||||
|
||||
nice_target_sources(test_update_verify ${src_loc}
|
||||
PRIVATE
|
||||
core/update_verify.cpp
|
||||
core/update_verify.h
|
||||
tests/test_update_verify.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test_update_verify
|
||||
PRIVATE
|
||||
desktop-app::external_qt
|
||||
desktop-app::external_openssl
|
||||
)
|
||||
|
||||
set_target_properties(test_update_verify PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
|
||||
add_dependencies(Telegram test_update_verify)
|
||||
|
||||
add_executable(test_text WIN32)
|
||||
init_target(test_text "(tests)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user