Compare commits

..

2 Commits
main ... 0.41.2

Author SHA1 Message Date
Sergey Skrobotov
e465b77cb6 Bump to version v0.41.2 2024-03-08 16:48:36 -08:00
Sergey Skrobotov
4388fac6a7 Revert "zkgroup: Implement GroupSendEndorsements"
This reverts commit 1c8fd06486.
2024-03-08 15:56:46 -08:00
1488 changed files with 57440 additions and 222907 deletions

View File

@ -1,6 +0,0 @@
[advisories]
ignore = [
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained, used by libcrux
"RUSTSEC-2024-0436", # paste is unmaintained, used by libsignal-bridge
"RUSTSEC-2025-0141", # bincode is unmaintained, used by zkgroup
]

View File

@ -1 +0,0 @@
0.29.2

View File

@ -1,11 +1 @@
too-many-arguments-threshold = 8
disallowed-methods = [
{ path = "jni::JNIEnv::find_class", reason = "use lookup helper instead" },
{ path = "jni::JNIEnv::call_method", reason = "use helper method instead" },
{ path = "jni::JNIEnv::call_method_unchecked", reason = "use helper method instead" },
{ path = "jni::JNIEnv::call_static_method", reason = "use helper method instead" },
{ path = "jni::JNIEnv::call_static_method_unchecked", reason = "use helper method instead" },
{ path = "jni::JNIEnv::new_object", reason = "use helper method instead" },
{ path = "jni::JNIEnv::new_object_unchecked", reason = "use helper method instead" },
]
allow-unwrap-in-tests = true

View File

@ -37,7 +37,3 @@ indent_size = 4
[*.swift]
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 4

10
.gitattributes vendored
View File

@ -3,11 +3,5 @@ acknowledgments/acknowledgments.* -merge -text
acknowledgments/acknowledgments.*.hbs merge text=auto
# Treat encrypted and unencrypted message backup files as binary
**/*.binproto binary
**/*.binproto.encrypted binary
# Avoid Windows line-endings for files compared literally.
**/*.expected.json text eol=lf
**/*.kt.in linguist-language=Kotlin
**/*.ts.in linguist-language=TypeScript
*.binproto binary
*.binproto.encrypted binary

View File

@ -1,24 +0,0 @@
self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels:
# Used in Slow Tests' AArch64 Linux Tests.
- ubuntu-24.04-arm64-4-cores
# This is... not a custom worker label, but it's not included in actionlint for some reason.
# See: https://github.com/rhysd/actionlint/blob/f9408506b4c7f9cda1263bca8166271f65e65c3d/rule_runner_label.go#L29
- windows-latest-4-cores
# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.
# Empty array means no configuration variable is allowed.
config-variables: null
# Configuration for file paths. The keys are glob patterns to match to file
# paths relative to the repository root. The values are the configurations for
# the file paths. Note that the path separator is always '/'.
# The following configurations are available.
#
# "ignore" is an array of regular expression patterns. Matched error messages
# are ignored. This is similar to the "-ignore" command line option.
paths:
# .github/workflows/**/*.yml:
# ignore: []

View File

@ -1,70 +0,0 @@
name: 'Restore Cargo Cache'
description: 'Restore cargo and build cache with appropriate keys'
inputs:
job-name:
description: 'Name for the cache (e.g., rust-nightly, node, java)'
required: true
toolchain:
description: 'Optional rustup toolchain spec to override the repo default'
required: false
default: 'workspace'
outputs:
rustc-version:
description: 'Full rustc --version string for the resolved toolchain'
value: ${{ steps.calculate.outputs.rustc-version }}
cache-key-merge-base:
description: 'The merge base commit with origin/main'
value: ${{ steps.calculate.outputs['cache-key-merge-base'] }}
cache-key-current:
description: 'Hash of current working tree'
value: ${{ steps.calculate.outputs['cache-key-current'] }}
cache-key:
description: 'Full cache key used for cargo artifacts'
value: ${{ steps.calculate-primary-cache-key.outputs['cache-primary-key'] }}
runs:
using: 'composite'
steps:
- name: Calculate cache key inputs
id: calculate
shell: bash
run: python3 "${{ github.action_path }}/calculate_cache_keys.py" --toolchain "${{ inputs.toolchain }}" >> "$GITHUB_OUTPUT"
- name: Calculate primary cache key
id: calculate-primary-cache-key
shell: bash
run: echo "cache-primary-key=${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}" >> "$GITHUB_OUTPUT"
- name: Restore cargo cache
id: cache
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE != 'true' }}
uses: runs-on/cache/restore@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
with:
# The special handling for the Windows target path comes because we overwrite
# $CARGO_BUILD_TARGET_DIR in build_node_bridge.py because Visual Studio's CLI
# tools are not long-path aware.
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/registry/src
~/.cargo/git/db
~/.cargo/git/checkouts
target
${{ runner.os == 'Windows' && format('{0}\\libsignal', runner.temp) || '' }}
# Cache key strategy:
# - The GitHub Actions cache API treats an exact key match as authoritative and skips re-uploading.
# - We use the working tree hash as the final key component to ensure uniqueness per commit.
# - On cache miss, we fall back, in order, to:
# 1. Most recent cache from the last common ancestor with main.
# 1.1. Most recent cache from the last common ancestor with main's parent.
# 1.2. Most recent cache from the last common ancestor with main's grandparent.
# 2. Most recent cache for this job/OS/rustc combination.
# 3. Most recent cache for this job/OS combination.
# This yields perfect hits on reruns while still warming cold builds with close matches.
# See: https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#cache-key-matching
key: ${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-${{ steps.calculate.outputs['cache-key-current'] }}
restore-keys: |
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-parent'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-${{ steps.calculate.outputs['cache-key-merge-base-grandparent'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-${{ steps.calculate.outputs['rustc-version'] }}-
${{ inputs['job-name'] }}-${{ runner.os }}-

View File

@ -1,145 +0,0 @@
#!/usr/bin/env python3
"""Calculate cache keys for GitHub Actions workflow."""
import argparse
import hashlib
import pathlib
import subprocess
import sys
from typing import Optional, Sequence, Tuple
def run_command(cmd: Sequence[str], check: bool = True) -> Tuple[int, str, str]:
"""Run a command and return its exit code, stdout, and stderr."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=check,
)
return result.returncode, result.stdout.strip(), result.stderr.strip()
except subprocess.CalledProcessError as exc:
stdout = exc.stdout.strip() if exc.stdout else ''
stderr = exc.stderr.strip() if exc.stderr else ''
return exc.returncode, stdout, stderr
def get_merge_base() -> str:
"""Get the merge base commit with origin/main."""
# Deepen all currently tracked tags to help find the merge base
run_command(['git', 'fetch', '--deepen=100', 'origin'], check=False)
code, output, _ = run_command(
['git', 'merge-base', 'HEAD', 'origin/main'],
check=False,
)
if code != 0 or not output:
print('Warning: could not determine merge base', file=sys.stderr)
return 'none'
return output
def get_merge_base_parent(commit: str) -> str:
"""Get the parent commit of the given commit."""
returncode, output, _ = run_command(
['git', 'rev-parse', f'{commit}^'],
check=False,
)
if returncode != 0 or not output:
return 'none'
return output
def get_working_tree_hash() -> str:
"""Get a hash representing the current working tree state."""
returncode, working_tree_hash, _ = run_command(
['git', 'rev-parse', 'HEAD^{tree}'],
check=False,
)
if returncode != 0 or not working_tree_hash:
raise RuntimeError('Could not determine working tree hash')
return working_tree_hash
def read_rust_toolchain_file(path: pathlib.Path) -> Optional[str]:
"""Read the rust-toolchain file if present."""
if not path.exists():
return None
content = path.read_text(encoding='utf-8').strip()
return content or None
def resolve_toolchain(override: Optional[str]) -> str:
"""Determine which toolchain spec to use."""
if override:
return override
workspace_root = pathlib.Path.cwd()
toolchain_from_file = read_rust_toolchain_file(workspace_root / 'rust-toolchain')
if toolchain_from_file:
return toolchain_from_file
raise RuntimeError('Could not determine toolchain')
def get_rustc_version(toolchain: str) -> str:
"""Resolve the full rustc --version string for the given toolchain."""
if not toolchain:
return 'unknown'
returncode, stdout, stderr = run_command(['rustup', 'run', toolchain, 'rustc', '--version'], check=False)
if returncode == 0 and stdout:
return stdout
if stderr:
print(f'Warning: command rustup run {toolchain} rustc --version failed: {stderr}', file=sys.stderr)
raise RuntimeError(f'Could not determine rustc version for toolchain {toolchain}')
def main() -> None:
parser = argparse.ArgumentParser(description='Calculate cache keys')
parser.add_argument(
'--toolchain',
default='workspace',
help='Rust toolchain spec to use, or "workspace" to use the repository configuration.',
)
args = parser.parse_args()
requested = args.toolchain.strip()
if not requested or requested == 'workspace':
requested = None
toolchain = resolve_toolchain(requested)
rustc_version = get_rustc_version(toolchain)
rustc_version_hash = hashlib.sha256(rustc_version.encode()).hexdigest()[:32]
lca = get_merge_base()
lca_parent = get_merge_base_parent(lca)
lca_grandparent = get_merge_base_parent(lca_parent)
current = get_working_tree_hash()
print(f'rustc-version={rustc_version_hash}')
print(f'cache-key-merge-base={lca}')
print(f'cache-key-merge-base-parent={lca_parent}')
print(f'cache-key-merge-base-grandparent={lca_grandparent}')
print(f'cache-key-current={current}')
debug_parts = [
f'Toolchain={toolchain}',
f'RustcVersion={rustc_version}',
f'LCA={lca}',
f'LCAParent={lca_parent}',
f'LCAGrandparent={lca_grandparent}',
f'Current={current}',
]
print('Debug: ' + '; '.join(debug_parts), file=sys.stderr)
if __name__ == '__main__':
main()

View File

@ -1,23 +0,0 @@
name: 'Save Cargo Cache'
description: 'Save cargo and build cache artifacts with appropriate keys'
inputs:
key:
description: 'The cache key to save under'
required: true
runs:
using: 'composite'
steps:
- name: Save cargo cache
if: ${{ env.DO_CLEAN_BUILD_AND_POPULATE_CACHE == 'true' }}
uses: runs-on/cache/save@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
with:
# Keep this path list in sync with restore-cargo-cache/action.yml.
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/registry/src
~/.cargo/git/db
~/.cargo/git/checkouts
target
${{ runner.os == 'Windows' && format('{0}\\libsignal', runner.temp) || '' }}
key: ${{ inputs.key }}

63
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,63 @@
# Copyright 2022 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- acknowledged
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: true
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been closed due to inactivity.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 5
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# issues:
# exemptLabels:
# - confirmed

View File

@ -1,18 +0,0 @@
# Notes on GitHub Actions
## Why not use `actions/cache` in the Rust jobs?
In Sep 2024, the slowest part of `build_and_test.yml` was the main Rust job, which runs several Rust-related checks---some using our pinned nightly, others using our MSRV, and still others with both toolchains. The slowest *parts* of the job are just building things, and that's at least partly because each step requires slightly different configurations, making the rebuilds less incremental than they might otherwise be. The second slowest job is the Java one, which builds the main library in several slices.
It might be reasonable to try to cache some of this work, either using [`actions/cache`][] directly or another action built on top of it like [`Swatinem/rust-cache`][]. However, it's not clear how much of a benefit we'll actually get:
- Turning off `CARGO_INCREMENTAL` (as suggested by `rust-cache`) would save some space in our target directories, but we actually do build our local crates in a few different configurations, so we might make builds longer if we do that.
- Fetching dependencies takes about 1m out of our total time, not enough to be worth targeting specifically.
- We build with two different Rust toolchains, so any caching we do is doubled. The Java build only uses one toolchain, but it builds release instead of debug, and does multiple slices. If we fill up our entire cache quota (10GB) by accident, we lose most of the benefits as each job's cache evicts one of the other ones.
- Building with a lower debug info setting might save on the space of build intermediates, but is then testing something different than what people usually use at their desk.
[`actions/cache`]: https://github.com/actions/cache
[`Swatinem/rust-cache`]: https://github.com/Swatinem/rust-cache

View File

@ -1,72 +0,0 @@
name: "Integration - Android"
on:
workflow_dispatch:
inputs:
signal_android_branch:
description: 'Signal-Android branch to test against'
required: false
default: 'main'
type: string
env:
CARGO_TERM_COLOR: always
NDK_VERSION: 28.0.13004108
jobs:
android-integration:
name: Android Client Integration Test
runs-on: ubuntu-latest-8-cores
timeout-minutes: 60
steps:
- name: Checkout libsignal
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
path: libsignal
submodules: recursive
- name: Checkout Signal-Android
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: signalapp/Signal-Android
ref: ${{ inputs.signal_android_branch }}
path: Signal-Android
- run: 'echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"'
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- run: |
cd libsignal
rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
- name: Install protoc
run: |
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v32.0/protoc-32.0-linux-x86_64.zip
sudo unzip -o protoc-32.0-linux-x86_64.zip -d /usr/local bin/protoc
sudo unzip -o protoc-32.0-linux-x86_64.zip -d /usr/local 'include/*'
rm protoc-32.0-linux-x86_64.zip
- name: Install Android NDK
run: |
sudo "${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${NDK_VERSION}"
echo "ANDROID_NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/${NDK_VERSION}" >> "$GITHUB_ENV"
- name: Run Android's QA checks with local libsignal
run: ./gradlew qa --no-daemon --stacktrace -PlibsignalClientPath=../libsignal -F OFF
working-directory: Signal-Android
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
- name: Upload test results
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: test-results
path: |
Signal-Android/**/build/reports/
Signal-Android/**/build/test-results/
retention-days: 7

View File

@ -1,34 +1,18 @@
name: "[CI] Build and Test"
name: Build and Test
on:
push:
branches: [ main ]
pull_request: # all target branches
workflow_dispatch:
inputs:
skip_cargo_cache:
description: Skip cargo cache restore/save steps
default: false
type: boolean
# On PRs, "head_ref" is defined and is consistent across updates. On
# pushes, it's not defined, so we use "run_id", which is unique across
# every run; as a result, all actions on pushes will run to completion.
#
# Reference: https://docs.github.com/en/actions/using-jobs/using-concurrency
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
NDK_VERSION: 28.0.13004108
NDK_VERSION: 25.2.9519653
RUST_BACKTRACE: 1
LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION: 1.72
# For dev builds, include limited debug info in the output. See
# https://doc.rust-lang.org/cargo/reference/profiles.html#debug
CARGO_PROFILE_DEV_DEBUG: limited
DO_CLEAN_BUILD_AND_POPULATE_CACHE: ${{ github.ref == 'refs/heads/main' && 'true' || 'false' }}
SHOULD_USE_CARGO_CACHE: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ENDPOINT != '' && secrets.R2_BUCKET_NAME != '' && inputs.skip_cargo_cache != true }}
jobs:
changes:
@ -49,18 +33,14 @@ jobs:
rust_ios: ${{ steps.filter.outputs.rust_ios }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: dorny/paths-filter@9d7afb8d214ad99e78fbd4247752c4caed2b6e4c # v4.0
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: dorny/paths-filter@0bc4621a3135347011ad047f9ecf449bf72ce2bd # v3.0
id: filter
with:
filters: |
all: &all
- '.github/workflows/build_and_test.yml'
- '.github/actions/**'
- 'bin/**'
- 'rust/*'
- 'rust/!(bridge|protocol)/**'
@ -70,7 +50,6 @@ jobs:
- 'rust-toolchain'
- 'Cargo.toml'
- 'Cargo.lock'
- '.cargo/**' # overly conservative, but it's fine
rust:
- *all
- '.clippy.toml'
@ -105,20 +84,15 @@ jobs:
- '.gitignore'
- '.gitattributes'
- '.editorconfig'
- '.tool-versions'
- 'justfile'
- 'doc/**'
- name: Check pattern completeness
run: echo "::error file=.github/workflows/build_and_test.yml::File not included in any filter" && false
# `actionlint` does not like it when you write this like: `!contains(steps.filter.outputs.*, 'true')`
# It also does not include a way to inline ignore a single instance of a warning. C'est la vie.
if: ${{ !contains(toJSON(steps.filter.outputs), '"true"') }}
if: ${{ !contains(steps.filter.outputs.*, 'true') }}
rust:
name: Rust
runs-on: ubuntu-latest-4-cores
runs-on: ubuntu-latest
needs: changes
@ -132,270 +106,134 @@ jobs:
- version: nightly
toolchain: "$(cat rust-toolchain)"
- version: stable
# Extract 'rust-version' value from Cargo.toml.
toolchain: "$(yq '.workspace.package.rust-version' $(git rev-parse --show-toplevel)/Cargo.toml)"
timeout-minutes: 45
toolchain: "${LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION}"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- run: rustup toolchain install "${{ matrix.toolchain }}" --profile minimal --component rustfmt,clippy
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust-${{ matrix.version }}
toolchain: ${{ matrix.toolchain }}
- name: Build
run: cargo +${{ matrix.toolchain }} build --workspace --features libsignal-ffi/signal-media --verbose --keep-going
- name: Run tests
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --no-fail-fast -- --include-ignored
- name: Test run benches
# Run with a match-all regex to select all the benchmarks, which (confusingly) causes other tests to be skipped.
run: cargo +${{ matrix.toolchain }} test --workspace --benches --all-features --verbose --no-fail-fast '.*'
- name: Build bins and examples
run: cargo +${{ matrix.toolchain }} build --workspace --bins --examples --all-features --verbose --keep-going
- name: Clippy
run: cargo clippy --workspace --all-targets --all-features --keep-going -- -D warnings
if: matrix.version == 'nightly'
- name: Rust docs
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features --no-deps --document-private-items --keep-going
if: matrix.version == 'stable'
env:
RUSTDOCFLAGS: -D warnings
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust32:
name: Rust (32-bit testing)
runs-on: ubuntu-latest-4-cores
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
strategy:
fail-fast: false
matrix:
version: [nightly, stable]
include:
- version: nightly
toolchain: "$(cat rust-toolchain)"
- version: stable
# Extract 'rust-version' value from Cargo.toml.
toolchain: "$(yq '.workspace.package.rust-version' $(git rev-parse --show-toplevel)/Cargo.toml)"
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get install -U gcc-multilib g++-multilib
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: rustup toolchain install "${{ matrix.toolchain }}" --profile minimal --target i686-unknown-linux-gnu
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust32-${{ matrix.version }}
toolchain: ${{ matrix.toolchain }}
- name: Run tests (32-bit)
# Exclude signal-neon-futures because those tests run Node
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
env:
CFLAGS: "-msse2" # for BoringSSL
CXXFLAGS: "-msse2" # for BoringSSL
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust-fuzz-build:
name: Rust (Fuzz Targets)
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve latest stable MSRV toolchain
id: rust-fuzz-build-toolchain
run: echo "latest-stable-msrv-toolchain=$(yq '.workspace.package.rust-version' Cargo.toml)" >> "$GITHUB_OUTPUT"
- name: Install Rust stable toolchain
run: rustup toolchain install "${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }}" --profile minimal
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: rust-fuzz-build
toolchain: ${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }}
# We check the fuzz targets on stable because they don't have lockfiles,
# and crates don't generally support arbitrary nightly versions.
# See https://github.com/dtolnay/proc-macro2/issues/307 for an example.
- name: Check that the protocol fuzz target still builds
run: cargo +${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }} check --all-targets --keep-going
working-directory: rust/protocol/fuzz
env:
RUSTFLAGS: --cfg fuzzing
- name: Check that the attest fuzz target still builds
run: cargo +${{ steps.rust-fuzz-build-toolchain.outputs.latest-stable-msrv-toolchain }} check --all-targets --keep-going
working-directory: rust/attest/fuzz
env:
RUSTFLAGS: --cfg fuzzing
- name: Save cargo cache
uses: ./.github/actions/save-cargo-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
rust-fmt:
name: Rust (Formatting and Acknowledgments)
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve pinned nightly toolchain
id: rust-fmt-toolchain
run: echo "pinned-nightly-toolchain=$(cat rust-toolchain)" >> "$GITHUB_OUTPUT"
- name: Install pinned nightly toolchain
run: rustup toolchain install "${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }}" --profile minimal --component rustfmt
- run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal --component rustfmt,clippy
- name: Cache locally-built tools
uses: runs-on/cache@575425708ccb521bfce731e8d8a67f7f337b8954 # main as of 2026-04-10
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
with:
path: local-tools
key: local-tools-${{ runner.os }}-infra-${{ hashFiles('acknowledgments/cargo-about-version', '.taplo-cli-version') }}
key: ${{ runner.os }}-local-tools-${{ matrix.version }}-${{ hashFiles('acknowledgments/cargo-about-version') }}
- name: Build cargo-about if needed
run: cargo +stable install --version "$(cat acknowledgments/cargo-about-version)" --locked cargo-about --root local-tools
- name: Build taplo-cli if needed
run: cargo +stable install --version "$(cat .taplo-cli-version)" --locked taplo-cli --root local-tools
run: cargo +stable install --version $(cat acknowledgments/cargo-about-version) --locked cargo-about --root local-tools
if: matrix.version == 'nightly'
# This should be done before anything else
# because it also checks that the lockfile is up to date.
- name: Check for duplicate dependencies
run: ./bin/verify_duplicate_crates
- name: Cargo.toml formatting check
run: PATH="$PATH:$PWD/local-tools/bin" taplo format -c .taplo.toml --check
if: matrix.version == 'nightly'
- name: Rustfmt check
run: cargo +${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }} fmt --all -- --check
run: cargo fmt --all -- --check
if: matrix.version == 'nightly'
- name: Rustfmt check for cross-version-testing
run: cargo +${{ steps.rust-fmt-toolchain.outputs.pinned-nightly-toolchain }} fmt --all -- --check
run: cargo fmt --all -- --check
working-directory: rust/protocol/cross-version-testing
if: matrix.version == 'nightly'
- name: Check bridge versioning
run: ./bin/update_versions.py
if: matrix.version == 'nightly'
- name: Check acknowledgments
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh --check
run: PATH="$PATH:$PWD/local-tools/bin" ./bin/regenerate_acknowledgments.sh && git diff --name-status --exit-code acknowledgments
if: matrix.version == 'nightly'
java_android:
name: Java Android
- name: Build
run: cargo +${{ matrix.toolchain }} build --workspace --features libsignal-ffi/signal-media --verbose
runs-on: ubuntu-latest-4-cores
- name: Run tests
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose -- --include-ignored
- name: Test run benches
run: cargo +${{ matrix.toolchain }} test --workspace --benches --all-features --verbose
- name: Build bins and examples
run: cargo +${{ matrix.toolchain }} build --workspace --bins --examples --all-features --verbose
- name: Clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
if: matrix.version == 'nightly'
- name: cargo clean (reclaim disk space)
# Clean the contents of the target directory to avoid running out of disk space during the
# doc build.
run: cargo clean
if: matrix.version == 'stable'
- name: Rust docs
run: cargo +${{ matrix.toolchain }} doc --workspace --all-features
if: matrix.version == 'stable'
env:
RUSTFLAGS: -D warnings
# We check the fuzz targets on stable because they don't have lockfiles,
# and crates don't generally support arbitrary nightly versions.
# See https://github.com/dtolnay/proc-macro2/issues/307 for an example.
- name: cargo clean (reclaim disk space)
# Clean the contents of the target directory to avoid running out of disk space during the
# following steps.
run: cargo clean
if: matrix.version == 'stable'
- name: Check that the protocol fuzz target still builds
run: cargo +${{ matrix.toolchain }} check --all-targets
working-directory: rust/protocol/fuzz
env:
RUSTFLAGS: --cfg fuzzing
if: matrix.version == 'stable'
- name: Check that the attest fuzz target still builds
run: cargo +${{ matrix.toolchain }} check --all-targets
working-directory: rust/attest/fuzz
env:
RUSTFLAGS: --cfg fuzzing
if: matrix.version == 'stable'
rust32:
name: Rust (32-bit testing)
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
strategy:
fail-fast: false
matrix:
version: [nightly, stable]
include:
- version: nightly
toolchain: "$(cat rust-toolchain)"
- version: stable
toolchain: "${LIBSIGNAL_MINIMUM_SUPPORTED_RUST_VERSION}"
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
- run: rustup toolchain install ${{ matrix.toolchain }} --profile minimal --target i686-unknown-linux-gnu
- name: Run tests (32-bit)
# Exclude signal-neon-futures because those tests run Node
run: cargo +${{ matrix.toolchain }} test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures -- --include-ignored
java:
name: Java
runs-on: ubuntu-latest
needs: changes
@ -406,44 +244,25 @@ jobs:
if: ${{ needs.changes.outputs.java == 'true' }}
timeout-minutes: 45
steps:
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
submodules: recursive
# Download all commits so we can search for the merge base with origin/main.
fetch-depth: 0
- name: Install NDK
run: |
"${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${NDK_VERSION}"
run: sudo ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;${NDK_VERSION}"
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android,i686-linux-android
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-linux-android,armv7-linux-androideabi
- name: Verify that the JNI bindings are up to date
run: rust/bridge/jni/bin/gen_java_decl.py --verify
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: java-android
- run: ./gradlew --dependency-verification strict --warning-mode fail :android:build :android:assembleAndroidTest :android:lintDebug :android:packaging-test:assembleDebugAndroidTest :android:benchmarks:assembleReleaseAndroidTest -PandroidArchs=arm,arm64 -x :makeJniLibrariesDesktop | tee ./gradle-output.txt
- run: ./gradlew build assembleDebugAndroidTest android:lintDebug -PandroidArchs=arm,arm64 -PandroidTestingArchs=x86_64 | tee ./gradle-output.txt
working-directory: java
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
@ -451,86 +270,10 @@ jobs:
- run: "! grep WARNING ./gradle-output.txt"
working-directory: java
- run: java/check_code_size.py | tee ./check_code_size-output.txt
- run: java/check_code_size.py
env:
GH_TOKEN: ${{ github.token }}
- run: grep -v -F '***' ./check_code_size-output.txt >> "$GITHUB_STEP_SUMMARY"
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
java_jvm:
name: Java JVM
runs-on: ubuntu-latest-4-cores
needs: changes
permissions:
contents: read
if: ${{ needs.changes.outputs.java == 'true' }}
timeout-minutes: 45
steps:
- run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
fetch-depth: 0
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: cargo +stable install cbindgen
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: java-jvm
- name: Verify that the JNI bindings are up to date
run: rust/bridge/jni/bin/gen_java_decl.py --verify
- run: ./gradlew --dependency-verification strict --warning-mode fail build -PskipAndroid
working-directory: java
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
node:
name: Node
@ -538,33 +281,16 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest-4-cores, windows-latest-4-cores, macos-15-xlarge]
os: [ubuntu-latest, windows-latest, macos-latest]
needs: changes
if: ${{ needs.changes.outputs.node == 'true' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: node
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
# install nasm compiler for boring
- name: Install nasm
@ -572,58 +298,40 @@ jobs:
run: choco install nasm
shell: cmd
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
if: startsWith(matrix.os, 'ubuntu-')
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
if: matrix.os == 'ubuntu-latest'
- run: choco install protoc
if: startsWith(matrix.os, 'windows-')
if: matrix.os == 'windows-latest'
- run: brew install protobuf
if: startsWith(matrix.os, 'macos-')
if: matrix.os == 'macos-latest'
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: '.nvmrc'
- name: Verify that the Node bindings are up to date
run: cargo run -p libsignal-node-native_ts -- --verify
if: startsWith(matrix.os, 'ubuntu-')
run: rust/bridge/node/bin/gen_ts_decl.py --verify
if: matrix.os == 'ubuntu-latest'
- run: npm ci
- run: yarn install --frozen-lockfile
working-directory: node
- run: npm run build
- run: yarn tsc
working-directory: node
- run: npm run tsc
- run: yarn lint
if: matrix.os == 'ubuntu-latest'
working-directory: node
- run: npm run lint
if: startsWith(matrix.os, 'ubuntu-')
- run: yarn format -c
if: matrix.os == 'ubuntu-latest'
working-directory: node
- run: npm run format-check
if: startsWith(matrix.os, 'ubuntu-')
- run: yarn test
working-directory: node
- run: npm run test
working-directory: node
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
swift_package:
name: Swift Package
@ -633,36 +341,12 @@ jobs:
if: ${{ needs.changes.outputs.swift == 'true' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
job-name: swift-package
- name: Install protoc
run: ./bin/install_protoc_linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- run: swift/verify_error_codes.sh
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- name: Build libsignal-ffi
run: swift/build_ffi.sh -d -v --verify-ffi
@ -671,73 +355,45 @@ jobs:
run: swift test -v
working-directory: swift
- name: Build and run Swift benchmarks (in debug mode)
run: swift run -v Benchmarks --allow-debug-build
working-directory: swift/Benchmarks
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
# Disabled for now, broken on Linux in the Swift 6.0 release.
# See https://forums.swift.org/t/generate-documentation-failing-for-swift-6-pre-release/74534
# - name: Build Swift package documentation
# run: swift package plugin generate-documentation --analyze --warnings-as-errors
# working-directory: swift
- name: Build Swift package documentation
run: swift package plugin generate-documentation --analyze --warnings-as-errors
working-directory: swift
swift_cocoapod:
name: Swift CocoaPod
runs-on: macos-15-xlarge
runs-on: macOS-latest
needs: changes
if: ${{ needs.changes.outputs.swift == 'true' }}
timeout-minutes: 45
env:
LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH: 1
# For Swift 6.2. Can be removed when advancing to the macos-26 runner.
DEVELOPER_DIR: /Applications/Xcode_26.3.app
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: brew install protobuf swiftlint
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Check formatting
run: swift format --in-place --parallel --recursive . && git diff --exit-code .
run: swiftformat --swiftversion 5 --reporter github-actions-log --lint .
working-directory: swift
- name: Run lint
run: swiftlint lint --strict --reporter github-actions-logging
working-directory: swift
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-apple-ios-sim
- name: Restore cargo cache
id: rust-cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/restore-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
- name: Check out SignalCoreKit
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
job-name: swift-cocoapod
repository: signalapp/SignalCoreKit
path: SignalCoreKit
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios-sim
- run: brew install protobuf
# Build only the targets that `pod lib lint` will test building.
- name: Build for x86_64-apple-ios
run: swift/build_ffi.sh --release
env:
CARGO_BUILD_TARGET: x86_64-apple-ios
- name: Build for aarch64-apple-ios-sim
run: swift/build_ffi.sh --release
@ -745,18 +401,5 @@ jobs:
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
- name: Run pod lint
run: pod lib lint --verbose --platforms=ios --skip-tests
env:
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
- name: Save cargo cache
if: ${{ env.SHOULD_USE_CARGO_CACHE == 'true' }}
uses: ./.github/actions/save-cargo-cache
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1
RUNS_ON_S3_BUCKET_CACHE: ${{ secrets.R2_BUCKET_NAME }}
RUNS_ON_S3_BUCKET_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
with:
key: ${{ steps['rust-cache'].outputs['cache-key'] }}
# No import validation because it tries to build unsupported platforms (like 32-bit iOS).
run: pod lib lint --verbose --platforms=ios --include-podspecs=SignalCoreKit/SignalCoreKit.podspec --skip-import-validation

View File

@ -1,30 +0,0 @@
name: "[CI] Check Versions"
# We want to run this job on all changes, so that we do not have to risk breakage slipping
# through due to the set of files included in the version consistency check getting out of sync
# with the set of files checked by the test dispatch logic.
#
# Thus, this job explicitly does not depend on the "Classify Changes" job, like all the other
# jobs in Build and Test do. The lint job also just runs on a subset of changes. So, this ends
# up being a completely independent job.
on:
push:
branches: [ main ]
pull_request: # all target branches
workflow_dispatch: {}
jobs:
check_versions:
name: Check version number consistency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
# The update_versions.py script checks that the version numbers in the source code
# are consistent with themselves and the version number in RELEASE_NOTES.md.
# It exits with a non-zero exit code if they are not consistent.
- run: ./bin/update_versions.py

View File

@ -1,31 +0,0 @@
name: "[CI] Docs"
env:
MDBOOK_VERSION: "0.4.43"
on:
push:
branches: [ main ]
paths: ['doc/**', '.github/workflows/docs.yml']
pull_request:
paths: ['doc/**', '.github/workflows/docs.yml']
jobs:
docs:
name: Check docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Download mdbook ${{ env.MDBOOK_VERSION }}
run: mkdir ~/bin && curl -sSL https://github.com/rust-lang/mdBook/releases/download/v${{ env.MDBOOK_VERSION }}/mdbook-v${{ env.MDBOOK_VERSION }}-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory ~/bin
- run: ~/bin/mdbook build
working-directory: doc
- run: ~/bin/mdbook test
working-directory: doc

View File

@ -1,4 +1,4 @@
name: "Release - iOS"
name: Build iOS Artifacts
on:
workflow_dispatch:
@ -22,35 +22,31 @@ jobs:
# Needed for google-github-actions/auth.
id-token: 'write'
runs-on: macos-15-xlarge
timeout-minutes: 45
runs-on: macos-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Checking run eligibility
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const dryRun = ${{ inputs.dry_run }};
const refType = '${{ github.ref_type }}';
const refName = '${{ github.ref_name }}';
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
: `Running on '${refName}' ${refType}`);
if (refType !== 'tag' && !dryRun) {
core.setFailed("the action should either be launched on a tag or with a 'dry run' switch");
}
- id: archive-name
run: echo "name=libsignal-client-ios-build-v$(sed -En "s/${VERSION_REGEX}/\1/p" LibSignalClient.podspec).tar.gz" >> "$GITHUB_OUTPUT"
run: echo name=libsignal-client-ios-build-v$(sed -En "s/${VERSION_REGEX}/\1/p" LibSignalClient.podspec).tar.gz >> $GITHUB_OUTPUT
env:
VERSION_REGEX: "^.*[.]version += '(.+)'$"
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
- run: brew install protobuf
@ -69,24 +65,24 @@ jobs:
env:
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
- run: tar -c --auto-compress --no-mac-metadata -f '${{ steps.archive-name.outputs.name }}' target/*/release/libsignal_ffi.a
- run: tar -c --auto-compress --no-mac-metadata -f ${{ steps.archive-name.outputs.name }} target/*/release/libsignal_ffi.a
- run: shasum -a 256 '${{ steps.archive-name.outputs.name }}' | tee -a "$GITHUB_STEP_SUMMARY" '${{ steps.archive-name.outputs.name }}.sha256'
- run: 'shasum -a 256 ${{ steps.archive-name.outputs.name }} | tee -a $GITHUB_STEP_SUMMARY ${{ steps.archive-name.outputs.name }}.sha256'
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
- name: Attach artifact to the run
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
path: ${{ steps.archive-name.outputs.name }}
name: libsignal-client-ios
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
- uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
if: ${{ !inputs.dry_run }}
with:
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
- uses: google-github-actions/upload-cloud-storage@6397bd7208e18d13ba2619ee21b9873edc94427a # v3.0.0
- uses: google-github-actions/upload-cloud-storage@22121cd842b0d185e042e28d969925b538c33d77 # v2.1.0
if: ${{ !inputs.dry_run }}
with:
path: ${{ steps.archive-name.outputs.name }}
@ -94,7 +90,7 @@ jobs:
# This step is expected to fail if not run on a tag.
- name: Upload checksum to release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
uses: ncipollo/release-action@66b1844f0b7ef940787c9d128846d5ac09b3881f # v1.14
if: ${{ !inputs.dry_run }}
with:
allowUpdates: true

View File

@ -1,4 +1,4 @@
name: "Release - Java"
name: Upload Java libraries to Sonatype
run-name: ${{ github.workflow }} (${{ github.ref_name }})
on:
@ -21,37 +21,35 @@ jobs:
strategy:
matrix:
os: [windows-latest-8-cores, macos-15-xlarge]
os: [windows-latest, macos-latest]
include:
- os: windows-latest-8-cores
- os: macos-15-xlarge
additional-rust-target: x86_64-apple-darwin
- os: windows-latest
library: signal_jni.dll
- os: macos-latest
library: libsignal_jni.dylib
additional-rust-target: aarch64-apple-darwin
# Ubuntu binaries are built using Docker, below
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Checking run eligibility
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const dryRun = ${{ inputs.dry_run }};
const refType = '${{ github.ref_type }}';
const refName = '${{ github.ref_name }}';
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
: `Running on '${refName}' ${refType}`);
if (refType !== 'tag' && !dryRun) {
core.setFailed("the action should either be launched on a tag or with a 'dry run' switch");
}
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
- run: rustup target add "${{ matrix.additional-rust-target }}"
- run: rustup target add ${{ matrix.additional-rust-target }}
if: ${{ matrix.additional-rust-target != '' }}
# install nasm compiler for boring
@ -61,63 +59,43 @@ jobs:
shell: cmd
- run: choco install protoc
if: startsWith(matrix.os, 'windows-')
if: matrix.os == 'windows-latest'
- run: brew install protobuf
if: startsWith(matrix.os, 'macos-')
if: matrix.os == 'macos-latest'
- name: Build client for host
- name: Build for host (should be x86_64)
run: java/build_jni.sh desktop
shell: bash
- name: Build server for host
run: java/build_jni.sh server
shell: bash
- name: Build client for alternate target
- name: Build for alternate target (arm64)
run: java/build_jni.sh desktop
if: startsWith(matrix.os, 'macos-')
if: matrix.os == 'macos-latest'
env:
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
- name: Build server for alternate target
run: java/build_jni.sh server
if: startsWith(matrix.os, 'macos-')
env:
CARGO_BUILD_TARGET: ${{ matrix.additional-rust-target }}
- name: Merge library slices (for macOS)
# Using target/release/ for both the input and output wouldn't normally be ideal
# from a build system perspective, but we're going to immediately upload the merged library.
run: lipo -create target/release/${{ matrix.library }} target/${{ matrix.additional-rust-target }}/release/${{ matrix.library }} -output target/release/${{ matrix.library }}
if: matrix.os == 'macos-latest'
- name: Upload client libraries
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
- name: Upload library
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal-client libraries (${{matrix.os}})
path: |
java/client/src/main/resources/*.dll
java/client/src/main/resources/*.dylib
- name: Upload server libraries
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libsignal-server libraries (${{matrix.os}})
path: |
java/server/src/main/resources/*.dll
java/server/src/main/resources/*.dylib
name: libsignal_jni (${{matrix.os}})
path: target/release/${{ matrix.library }}
verify-rust:
name: Verify JNI bindings
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
- run: sudo apt-get install -U protobuf-compiler
- run: cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- name: Verify that the JNI bindings are up to date
run: rust/bridge/jni/bin/gen_java_decl.py --verify
@ -125,35 +103,21 @@ jobs:
publish:
name: Build for production and publish
permissions:
contents: read
# Needed for google-github-actions/auth.
id-token: write
runs-on: ubuntu-latest-8-cores
runs-on: ubuntu-latest
needs: [build, verify-rust]
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Download built client libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Download built libraries
id: download
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
path: java/client/src/main/resources
pattern: libsignal-client*
merge-multiple: true
path: artifacts
- name: Download built server libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: java/server/src/main/resources
pattern: libsignal-server*
merge-multiple: true
- name: Copy libraries
run: mv ${{ steps.download.outputs.download-path }}/*/* java/shared/resources && find java/shared/resources
- run: make
if: ${{ inputs.dry_run }}
@ -161,38 +125,31 @@ jobs:
- name: Upload libsignal-android
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal-android
path: java/android/build/outputs/aar/libsignal-android-release.aar
- name: Upload libsignal-client
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal-client
path: java/client/build/libs/libsignal-client-*.jar
- name: Upload libsignal-server
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal-server
path: java/server/build/libs/libsignal-server-*.jar
- id: gcp-auth
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
if: ${{ !inputs.dry_run }}
with:
workload_identity_provider: 'projects/741367068918/locations/global/workloadIdentityPools/github/providers/github-actions'
service_account: 'github-actions@signal-build-artifacts.iam.gserviceaccount.com'
token_format: 'access_token'
- run: make publish_java
if: ${{ !inputs.dry_run }}
working-directory: java
env:
CLOUDSDK_AUTH_ACCESS_TOKEN: ${{ steps.gcp-auth.outputs.access_token }}
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEYID }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }}
# ASCII-armored PGP secret key

View File

@ -1,13 +1,13 @@
name: "[CI] Lints"
name: Lints
# This is in a separate job because we have shell scripts scattered across all our targets,
# *and* some of them have common dependencies.
on:
push:
branches: [ main ]
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
pull_request:
paths: ['**/*.sh', '**/*.py', '.github/workflows/*.yml']
paths: ['**/*.sh', '**/*.py', '.github/workflows/lints.yml']
jobs:
lint:
@ -16,22 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get install -U python3-flake8 python3-flake8-comprehensions python3-flake8-deprecated python3-flake8-import-order python3-flake8-quotes python3-mypy
- run: |
shopt -s globstar
shellcheck -- **/*.sh bin/verify_duplicate_crates bin/adb-run-test
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: pip3 install flake8 mypy
- run: shellcheck **/*.sh bin/verify_duplicate_crates bin/adb-run-test
- run: python3 -m flake8 .
- run: python3 -m mypy . --python-version 3.9 --strict
env:
# Some scripts modify sys.path to fetch from ./bin
MYPYPATH: ./bin
- name: Install actionlint
run: |
mkdir -p "$HOME/bin"
curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/e7d448ef7507c20fc4c88a95d0c448b848cd6127/scripts/download-actionlint.bash \
| bash -s -- 1.7.8 "$HOME/bin"
echo "$HOME/bin" >> "$GITHUB_PATH"
- run: actionlint
# Only include typed Python scripts here.
- run: python3 -m mypy bin/fetch_archive.py --python-version 3.8 --strict

View File

@ -1,4 +1,4 @@
name: "Release - NPM"
name: Publish to NPM
on:
workflow_dispatch:
@ -24,36 +24,34 @@ jobs:
strategy:
matrix:
os: [windows-latest-8-cores, macos-15-xlarge]
os: [windows-latest, macos-11]
include:
- os: macos-15-xlarge
rust-cross-target: x86_64-apple-darwin
- os: windows-latest-8-cores
rust-cross-target: aarch64-pc-windows-msvc
- os: macos-11
arm64-rust-target: aarch64-apple-darwin
- os: windows-latest
arm64-rust-target: aarch64-pc-windows-msvc
# This can be removed when we update to a Node version that officially supports win-arm64.
custom-arm64-dist-url: https://unofficial-builds.nodejs.org/download/release
# Ubuntu binaries are built using Docker, below
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Checking run eligibility
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const dryRun = ${{ inputs.dry_run }};
const refType = '${{ github.ref_type }}';
const refName = '${{ github.ref_name }}';
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
console.log(dryRun
? `Running in 'dry run' mode on '${refName}' ${refType}`
: `Running on '${refName}' ${refType}`);
if (refType !== 'tag' && !dryRun) {
core.setFailed("the action should either be launched on a tag or with a 'dry run' switch");
}
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target "${{ matrix.rust-cross-target }}"
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target ${{ matrix.arm64-rust-target }}
# install nasm compiler for boring
- name: (Windows) Install nasm
@ -67,162 +65,107 @@ jobs:
- run: brew install protobuf
if: startsWith(matrix.os, 'macos')
- run: cargo +stable install dump_syms --locked --no-default-features --features cli
- name: Get Node version from .nvmrc
id: get-nvm-version
shell: bash
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
run: echo "node-version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.24
with:
node-version-file: '.nvmrc'
- run: npx yarn install --ignore-scripts --frozen-lockfile
working-directory: node
- name: Build for arm64
run: npm run build -- --arch arm64 --copy-to-prebuilds
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }} --arch arm64
working-directory: node
env:
npm_config_dist_url: ${{ matrix.custom-arm64-dist-url }}
- name: Save arm64 debug info
run: mv build/Release/*-debuginfo.* .
- name: Build for the host (should be x64)
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }}
working-directory: node
shell: bash
- name: Build for x64
run: npm run build -- --arch x64 --copy-to-prebuilds
working-directory: node
- name: Save x64 debug info
run: mv build/Release/*-debuginfo.* .
working-directory: node
shell: bash
- name: Upload library
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal_client (${{matrix.os}})
path: node/prebuilds/*
- name: Upload debug info
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Debug info (${{matrix.os}})
path: |
node/*-debuginfo.*
!node/*.sha256
build-docker:
name: Build (Ubuntu via Docker)
runs-on: ubuntu-latest-8-cores
timeout-minutes: 45
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: node/docker-prebuildify.sh
- name: Upload library
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: libsignal_client (ubuntu-docker)
path: node/prebuilds/*
- name: Upload debug info
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: Debug info (ubuntu-docker)
path: |
node/*-debuginfo.*
!node/*.sha256
verify-rust:
name: Verify Node bindings
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal
- run: sudo apt-get install -U protobuf-compiler
- run: sudo apt-get update && sudo apt-get install protobuf-compiler
- name: Verify that the Node bindings are up to date
run: cargo run -p libsignal-node-native_ts -- --verify
run: rust/bridge/node/bin/gen_ts_decl.py --verify
publish:
name: Publish
permissions:
# Required for OIDC
id-token: write
# Needed for ncipollo/release-action.
contents: write
runs-on: ubuntu-latest
needs: [build, build-docker, verify-rust]
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: '.nvmrc'
registry-url: 'https://registry.npmjs.org/'
- name: Download built libraries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
id: download
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
pattern: libsignal_client*
path: node/prebuilds
merge-multiple: true
path: artifacts
- name: Download debug info
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: Debug info*
path: debuginfo
merge-multiple: true
- name: Copy libraries
run: mkdir node/prebuilds && mv ${{ steps.download.outputs.download-path }}/*/* node/prebuilds && find node/prebuilds
- name: Update npm
run: npm install -g npm@latest
- run: npm ci
- run: yarn install --frozen-lockfile
working-directory: node
- run: npm run tsc
- run: yarn tsc
working-directory: node
- run: npm run lint
- run: yarn lint
working-directory: node
- run: npm run format -c
- run: yarn format -c
working-directory: node
- run: npm run test
- run: yarn test
working-directory: node
env:
PREBUILDS_ONLY: 1
- run: npm publish --tag '${{ github.event.inputs.npm_tag }}' --access public ${{ inputs.dry_run && '--dry-run' || ''}}
- if: ${{ !inputs.dry_run }}
run: npm publish --tag ${{ github.event.inputs.npm_tag }} --access public
working-directory: node
# This step is expected to fail if not run on a tag.
- name: Upload debug info to release
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20
if: ${{ !inputs.dry_run }}
with:
allowUpdates: true
artifactErrorsFailBuild: true
artifacts: debuginfo/*-debuginfo.*
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,33 +0,0 @@
name: "[CI] Check release notes"
# This is in a separate job because it only runs on pull requests and triggers
# on label changes in addition to code changes.
on:
pull_request:
types: [opened, reopened, synchronize, labeled, unlabeled, converted_to_draft, ready_for_review]
# all target branches
env:
LABEL_NAME: no release notes
jobs:
check:
name: Check for release notes
# Only check non-draft PRs in Signal's private repo.
if: (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private') && github.event.pull_request.draft == false)
runs-on: ubuntu-latest
permissions:
# Needed to read the list of files modified by the pull request.
pull-requests: read
steps:
- name: Check for release notes change
uses: brettcannon/check-for-changed-files@871d7b8b5917a4f6f06662e2262e8ffc51dff6d1 # v1.2.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
skip-label: ${{ env.LABEL_NAME }}
file-pattern: |
RELEASE_NOTES.md
failure-message: "RELEASE_NOTES.md is unchanged. If that's intentional, set the '${{ env.LABEL_NAME }}' tag"

View File

@ -1,11 +1,4 @@
name: "Integration - Slow Tests"
env:
ANDROID_NDK_VERSION: 28.0.13004108
LIBSIGNAL_TESTING_CDSI_ENCLAVE_SECRET: ${{secrets.CDSI_ENCLAVE_SECRET}}
LIBSIGNAL_TESTING_PROXY_SERVER: ${{secrets.LIBSIGNAL_TESTING_PROXY_SERVER}}
LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS: true
LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET: ${{secrets.LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET}}
name: Slow Tests
on:
schedule:
@ -15,166 +8,51 @@ on:
# We pick 8:25 UTC, aiming for "later than PST/UTC-8 night work" and
# "earlier than ADT/UTC-3 morning work".
- cron: '25 8 * * *'
workflow_dispatch:
inputs:
ios_runner:
description: 'Runner for iOS tests'
required: true
# This is redundant with specifying it at the use site, but makes it appear in the website UI.
# See https://github.com/actions/runner-images/blob/main/README.md#available-images
default: 'macos-15-xlarge'
ignore_kt_tests:
type: boolean
description: 'Skip Key Transparency tests (sets LIBSIGNAL_TESTING_IGNORE_KT_TESTS)'
default: false
bigger_workers:
type: boolean
description: 'Run on larger, more expensive workers for faster results'
default: false
workflow_dispatch: {} # no parameters
jobs:
check-up-to-date:
name: Already up to date?
runs-on: ubuntu-latest
if: ${{ github.event_name == 'schedule' && github.repository_owner == 'signalapp' && endsWith(github.repository, '-private') }}
outputs:
has-changes: ${{ steps.check.outputs.has-changes }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: git log --after '24 hours ago' --exit-code || echo 'has-changes=true' >> $GITHUB_OUTPUT
id: check
java-docker:
name: Java (Docker)
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
runs-on: ubuntu-latest
needs: [check-up-to-date]
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- name: Print KT env toggle
run: |
echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=${LIBSIGNAL_TESTING_IGNORE_KT_TESTS:-<unset>}"
- run: make -C java
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: make -C java java_test
- name: Upload JNI libraries
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: jniLibs
path: java/android/src/main/jniLibs/*
path: java/android/src/androidTest/jniLibs/*
retention-days: 2
- name: Upload full JARs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jars
path: java/*/build/libs/*
retention-days: 2
- name: Upload full AARs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: aars
path: java/android/build/outputs/aar/*
retention-days: 2
java-docker-secondary:
name: Java (Secondary Docker)
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: make -C java
- name: Upload full JARs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: jars-secondary
path: java/*/build/libs/*
retention-days: 2
- name: Upload full AARs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: aars-secondary
path: java/android/build/outputs/aar/*
retention-days: 2
java-reproducibility:
name: Verify Java Reproducible Build
runs-on: ubuntu-latest
needs: [java-docker, java-docker-secondary]
if: ${{ needs.java-docker.result == 'success' && needs.java-docker-secondary.result == 'success' }}
steps:
- name: Download jars
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jars
path: a/jars/
- name: Download jars (secondary)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: jars-secondary
path: b/jars/
- name: Download aars
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: aars
path: a/aars/
- name: Download aars (secondary)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: aars-secondary
path: b/aars/
- run: diff -qr a/ b/
java-extra-bridging-checks:
name: Java with runtime bridging checks
runs-on: ubuntu-latest-4-cores
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: sudo apt-get install -U protobuf-compiler
- run: ./gradlew :client:test :server:test -PskipAndroid -PjniTypeTagging -PjniCheckAnnotations
working-directory: java
android-emulator-tests:
name: Android Emulator Tests
# For hardware acceleration; see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-8-cores' || 'ubuntu-latest-4-cores' }}
env:
# Proxy server tests will fail on Android API prior to 28
LIBSIGNAL_TESTING_PROXY_SERVER: ${{ fromJSON(matrix.api_level) >= 28 && secrets.LIBSIGNAL_TESTING_PROXY_SERVER || '' }}
runs-on: ubuntu-latest-4-cores
needs: [java-docker]
if: ${{ needs.java-docker.result == 'success' }}
timeout-minutes: 45
if: ${{ always() && needs.java-docker.result == 'success' }}
strategy:
fail-fast: false
matrix:
# 23 is our minimal API level
# 33 is our target API level
include:
- api_level: 23
arch: x86
- api_level: 23
arch: x86_64
- api_level: 33
arch: x86_64
arch: [x86, x86_64]
steps:
- run: 'echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"'
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
# For hardware acceleration; see https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
- name: Enable KVM group perms
run: |
@ -182,168 +60,68 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Download JNI libraries
id: download
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4
with:
name: jniLibs
path: java/android/src/main/jniLibs/
path: java/android/src/androidTest/jniLibs/
# From reactivecircus/android-emulator-runner
- name: AVD cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.arch }}-${{ matrix.api_level }}-linux
key: avd-${{ matrix.arch }}-21-linux
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1
with:
arch: ${{ matrix.arch }}
api-level: ${{ matrix.api_level }}
ndk: ${{ env.ANDROID_NDK_VERSION }}
api-level: 21
force-avd-creation: false
disk-size: 4096M
emulator-options: -no-window -noaudio -no-boot-anim -no-metrics
emulator-options: -no-window -noaudio -no-boot-anim
script: echo "Generated AVD snapshot for caching."
- name: Run tests
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 # v2.30.1
with:
arch: ${{ matrix.arch }}
api-level: ${{ matrix.api_level }}
ndk: ${{ env.ANDROID_NDK_VERSION }}
disk-size: 4096M
api-level: 21
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim -no-metrics
script: |
adb logcat -c
adb logcat > logcat.log &
LOGCAT_PID="$!"
echo "LOGCAT_PID=$LOGCAT_PID" >> "$GITHUB_ENV"
./gradlew android:connectedCheck android:packaging-test:connectedCheck -x makeJniLibrariesDesktop -x android:makeJniLibraries
emulator-options: -no-snapshot-save -no-window -noaudio -no-boot-anim
script: ./gradlew android:connectedCheck -x makeJniLibrariesDesktop -x android:makeJniLibraries -x android:makeTestJniLibraries
working-directory: java
- name: Stop logcat
if: always()
run: kill ${{ env.LOGCAT_PID}} || true
- name: Upload logcat logs
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: logcat-logs-api${{ matrix.api_level }}-${{ matrix.arch }}
path: java/logcat.log
retention-days: 2
aarch64:
name: AArch64 Linux Tests
runs-on: ubuntu-24.04-arm64-4-cores
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- run: sudo apt-get install -U protobuf-compiler
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
# Skip building for Android; that's handled by the previous tests.
- run: ./gradlew build -PskipAndroid | tee ./gradle-output.txt
working-directory: java
shell: bash # Explicitly setting the shell turns on pipefail in GitHub Actions
# Check for -Xcheck:jni warnings manually; Gradle doesn't capture them for some reason.
- run: "! grep WARNING ./gradle-output.txt"
working-directory: java
node-docker:
name: Node (Ubuntu via Docker)
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
runs-on: ubuntu-latest
needs: [check-up-to-date]
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: node/docker-prebuildify.sh
- run: npm ci && npm run tsc && npm run test
- run: yarn tsc && yarn test
working-directory: node
env:
PREBUILDS_ONLY: 1
- name: Upload prebuilds
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: node-prebuilds
path: node/prebuilds
retention-days: 2
node-docker-secondary:
name: Node (Secondary Ubuntu via Docker)
runs-on: ${{ inputs.bigger_workers == true && 'ubuntu-latest-4-cores' || 'ubuntu-latest' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: node/docker-prebuildify.sh
- name: Upload prebuilds
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: node-prebuilds-secondary
path: node/prebuilds
retention-days: 2
node-reproducibility:
name: Verify Desktop Linux Reproducible Build
runs-on: ubuntu-latest
needs: [node-docker, node-docker-secondary]
if: ${{ needs.node-docker.result == 'success' && needs.node-docker-secondary.result == 'success' }}
steps:
- name: Download prebuilds
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: node-prebuilds
path: a/prebuilds/
- name: Download prebuilds (secondary)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: node-prebuilds-secondary
path: b/prebuilds/
- run: diff -qr a/ b/
node-windows-arm64:
name: Node (Windows ARM64 cross-compile)
runs-on: windows-latest
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
needs: [check-up-to-date]
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target aarch64-pc-windows-msvc
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target aarch64-pc-windows-msvc
# install nasm compiler for boring
- name: Install nasm
run: choco install nasm
@ -354,216 +132,102 @@ jobs:
- name: Get Node version from .nvmrc
id: get-nvm-version
shell: bash
run: echo "node-version=$(cat .nvmrc)" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
run: echo "node-version=$(cat .nvmrc)" >> $GITHUB_OUTPUT
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version-file: '.nvmrc'
- name: Build for arm64
run: npm run build -- --arch arm64 --copy-to-prebuilds
- run: npx yarn install --ignore-scripts --frozen-lockfile
working-directory: node
- name: Build for arm64
run: npx prebuildify --napi -t ${{ steps.get-nvm-version.outputs.node-version }} --arch arm64
working-directory: node
env:
npm_config_dist_url: https://unofficial-builds.nodejs.org/download/release
swift-cocoapod:
name: Swift CocoaPod (all architectures)
runs-on: ${{ inputs.ios_runner || 'macos-15-xlarge' }}
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
# Selects a specific version of Xcode to build and test with. Check the runner
# image (in https://github.com/actions/runner-images/) to see which ones are available. You can
# also set this on specific steps if necessary.
env:
# For Swift 6.2. Can be commented out again when the default runner moves to macos-26.
DEVELOPER_DIR: /Applications/Xcode_26.3.app
runs-on: macOS-latest
needs: [check-up-to-date]
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
- name: Check out SignalCoreKit
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: signalapp/SignalCoreKit
path: SignalCoreKit
- run: rustup toolchain install $(cat rust-toolchain) --profile minimal --target x86_64-apple-ios,aarch64-apple-ios,aarch64-apple-ios-sim --component rust-src
- run: brew install protobuf
- name: Build for x86_64-apple-ios
run: swift/build_ffi.sh --release
env:
CARGO_BUILD_TARGET: x86_64-apple-ios
- name: Build for aarch64-apple-ios
run: swift/build_ffi.sh --release
env:
CARGO_BUILD_TARGET: aarch64-apple-ios
# Build the simulator architectures for `pod lib lint` below.
- name: Build for x86_64-apple-ios
run: swift/build_ffi.sh --release
env:
CARGO_BUILD_TARGET: x86_64-apple-ios
- name: Build for aarch64-apple-ios-sim
run: swift/build_ffi.sh --release
env:
CARGO_BUILD_TARGET: aarch64-apple-ios-sim
# We run this for the non-hermetic tests; it's otherwise the same as regular CI.
- name: Run pod lint
run: pod lib lint --verbose --platforms=ios
env:
LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES: 1
# Make sure we can build for device, just for completeness.
- name: Set up testing workspace
run: pod install
working-directory: swift/cocoapods-testing
- name: Manually build for device
run: xcodebuild -scheme LibSignalClient -sdk iphoneos build-for-testing
working-directory: swift/cocoapods-testing
- name: Build in Release for device as well
run: xcodebuild -scheme LibSignalClient -sdk iphoneos -configuration Release
working-directory: swift/cocoapods-testing
# No import validation because it tries to build unsupported platforms (like 32-bit iOS).
run: pod lib lint --verbose --platforms=ios --include-podspecs=SignalCoreKit/SignalCoreKit.podspec --skip-import-validation
rust-stable-testing:
name: Rust tests (using latest stable)
runs-on: ubuntu-latest-4-cores
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
runs-on: ubuntu-latest
needs: [check-up-to-date]
if: ${{ always() && (needs.check-up-to-date.outputs.has-changes || github.event_name != 'schedule') }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- name: Enable KT skip flag
if: ${{ github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.ignore_kt_tests == true) }}
run: echo "LIBSIGNAL_TESTING_IGNORE_KT_TESTS=true" >> "$GITHUB_ENV"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- run: sudo apt-get install -U gcc-multilib g++-multilib protobuf-compiler
- run: sudo apt-get update && sudo apt-get install gcc-multilib g++-multilib protobuf-compiler
- run: rustup +stable target add i686-unknown-linux-gnu
- run: cargo +stable install cargo-audit
- run: cargo +stable audit -D warnings
- name: Run tests
run: cargo +stable test --workspace --all-features --verbose --no-fail-fast -- --include-ignored
env:
RUST_LOG: debug
- name: Run hermetic tests without network access
run: |
TEST_CMD='unset LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS LIBSIGNAL_TESTING_CDSI_ENCLAVE_SECRET LIBSIGNAL_TESTING_SVRB_ENCLAVE_SECRET && \
cargo +stable test --workspace --all-features --verbose --no-fail-fast -- --include-ignored'
./bin/run_with_network_isolation.sh "${TEST_CMD}"
env:
RUST_LOG: debug
run: cargo +stable test --workspace --all-features --verbose -- --include-ignored
- name: Test run benches
# Run with a match-all regex to select all the benchmarks, which (confusingly) causes other tests to be skipped.
run: cargo +stable test --workspace --benches --all-features --no-fail-fast --verbose '.*'
env:
RUST_LOG: debug
run: cargo +stable test --workspace --benches --all-features --verbose
- name: Build bins and examples
run: cargo +stable build --workspace --bins --examples --all-features --verbose --keep-going
- name: Run libsignal-net smoke tests
run: cargo +stable run --example chat_smoke_test -p libsignal-net --features="test-util" -- --try-all-routes staging
env:
RUST_LOG: debug
run: cargo +stable build --workspace --bins --examples --all-features --verbose
- name: Run tests (32-bit)
# Exclude signal-neon-futures because those tests run Node
run: cargo +stable test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures --no-fail-fast -- --include-ignored
env:
RUST_LOG: debug
CFLAGS: "-msse2" # for BoringSSL
CXXFLAGS: "-msse2" # for BoringSSL
run: cargo +stable test --workspace --all-features --verbose --target i686-unknown-linux-gnu --exclude signal-neon-futures -- --include-ignored
- name: cargo clean (reclaim disk space)
# Clean the contents of the target directory to avoid running out of disk space during the
# following steps.
run: cargo +stable clean
- name: Run libsignal-protocol cross-version tests
run: cargo +stable test --no-fail-fast
run: cargo +stable test
working-directory: rust/protocol/cross-version-testing
env:
RUST_LOG: debug
- name: Run libsignal-protocol cross-version tests (32-bit)
run: cargo +stable test --target i686-unknown-linux-gnu --no-fail-fast
run: cargo +stable test --target i686-unknown-linux-gnu
working-directory: rust/protocol/cross-version-testing
env:
RUST_LOG: debug
# We don't run Clippy because GitHub silently updates `stable` and that can introduce new lints,
# and we don't have a guarantee that any particular pinned nightly can build older libsignals.
rust-fuzzing:
name: Rust fuzzing
runs-on: ubuntu-latest-4-cores
if: ${{ github.event_name != 'schedule' || (github.repository_owner == 'signalapp' && endsWith(github.repository, '-private')) }}
timeout-minutes: 45
env:
CARGO_FUZZ_VERSION: 0.12.0
FUZZ_TIME_SECONDS: 60
FUZZ_JOBS: 4 # because this is a "4-cores" runner
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
submodules: recursive
- run: sudo apt-get install -U protobuf-compiler
- run: rustup toolchain install "$(cat rust-toolchain)" --profile minimal
- name: Cache cargo-fuzz
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: local-tools
key: ${{ runner.os }}-fuzzing-local-tools-${{ env.CARGO_FUZZ_VERSION }}
- name: Install cargo-fuzz if needed
run: cargo +stable install --version ${{ env.CARGO_FUZZ_VERSION }} --locked cargo-fuzz --root local-tools
- run: echo "$PWD/local-tools/bin" >> "$GITHUB_PATH"
# Note that these invocations will use libsignal's pinned toolchain,
# but it's always possible cargo-fuzz will want an older/newer nightly.
- run: cargo fuzz build interaction && cargo fuzz run interaction -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/protocol
- run: cargo fuzz build sealed_sender_v2 && cargo fuzz run sealed_sender_v2 -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/protocol
- run: cargo fuzz build session_management && cargo fuzz run session_management -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/protocol
- run: cargo fuzz build dcap && cargo fuzz run dcap -j${{ env.FUZZ_JOBS }} -- -max_total_time=${{ env.FUZZ_TIME_SECONDS }}
working-directory: rust/attest
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: failure()
with:
name: fuzzing-artifacts-${{ github.sha }}
path: rust/*/fuzz/artifacts
# This isn't fuzzing, but we have to do it with a nightly compiler, so we're going to tack it on to this job.
- name: Build everything with no lockfile and -Zdirect-minimal-versions
run: mkdir minimal-versions && CARGO_RESOLVER_LOCKFILE_PATH=minimal-versions/Cargo.lock bin/without_building_boring.sh cargo check --workspace --all-targets --all-features --verbose --keep-going -Zdirect-minimal-versions -Zlockfile-path
report-failures:
report_failures:
name: Report Failures
runs-on: ubuntu-latest
needs:
- java-docker
- java-reproducibility
- java-extra-bridging-checks
- android-emulator-tests
- aarch64
- node-docker
- node-reproducibility
- node-windows-arm64
- swift-cocoapod
- rust-stable-testing
- rust-fuzzing
needs: [java-docker, android-emulator-tests, node-docker, node-windows-arm64, swift-cocoapod, rust-stable-testing]
if: ${{ failure() && github.event_name == 'schedule' }}
permissions:
@ -572,13 +236,12 @@ jobs:
contents: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: 'Failed Slow Tests: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\n\n' +
'Note that a [later run](${{ github.server_url }}/${{ github.repository }}/actions/workflows/slow_tests.yml) may have succeeded.'
body: 'Failed Slow Tests: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>'
})

View File

@ -1,37 +0,0 @@
name: "[auto] Close stale issues and PRs"
on:
schedule:
- cron: '15 12 * * *' # 7:15 EST, early in a workday
workflow_dispatch: {} # no parameters
jobs:
stale:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
close-issue-message: >
This issue has been closed due to inactivity.
stale-pr-message: >
This PR has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
close-pr-message: >
This PR has been closed due to inactivity.
days-before-stale: 90
days-before-close: 7
stale-issue-label: stale
stale-pr-label: stale
# These are comma-separated lists, if we ever want more than one.
exempt-issue-labels: "acknowledged"
exempt-pr-labels: "acknowledged"
exempt-all-assignees: true
operations-per-run: 30

20
.gitignore vendored
View File

@ -1,8 +1,7 @@
.idea
/.idea
*.iml
/target
/swift/**/.build
/swift/**/.swiftpm
/swift/.build
/node/dist
/node/node_modules
/node/build
@ -21,17 +20,8 @@ java/.gradle
java/local.properties
java/android/src/*/jniLibs
java/android/src/main/assets/acknowledgments
java/backup-tool/bin/
java/client/bin/
java/client/src/main/resources/*.dylib
java/client/src/main/resources/*.so
java/client/src/main/resources/*.dll
java/server/bin/
java/server/src/main/resources/*.dylib
java/server/src/main/resources/*.so
java/server/src/main/resources/*.dll
java/shared/resources/*.dylib
java/shared/resources/*.so
java/shared/resources/*.dll
.DS_Store
Brewfile.lock.json
__pycache__

2
.nvmrc
View File

@ -1 +1 @@
24.13.0
18.15.0

View File

@ -1,3 +1 @@
imports_granularity = "Module"
group_imports = "StdExternalCrate"
style_edition = "2024"

View File

@ -1,9 +0,0 @@
{
"version": 1,
"lineLength": 120,
"indentation": { "spaces": 4 },
"indentConditionalCompilationBlocks": false,
"lineBreakBeforeEachArgument": true,
"lineBreakBetweenDeclarationAttributes": true,
"prioritizeKeepingFunctionOutputTogether": true
}

25
.swiftformat Normal file
View File

@ -0,0 +1,25 @@
#--header "\nCopyright {created.year} Signal Messenger, LLC.\nSPDX-License-Identifier: AGPL-3.0-only\n"
--disable hoistPatternLet
# Explicit self is better than implicit self.
--self insert
# Some arguments that it considers unused are used in doc comments, and replacing them with '_' is an error.
--stripunusedargs unnamed-only
--wraparguments before-first
--wrapcollections before-first
# Libsignal is a collection of many languages, remembering specific of each one is hard. Make it explicit.
--disable redundantinternal
# Ranges look better without spaces
--ranges no-space
# Pragmas should start at the begining of line.
--ifdef outdent
--indent 4
# Patters are not redundant, they show the shape of thing, they show the shape of things.
--disable redundantPattern
# Leave try in the innermost position.
--disable hoistTry
# Explicit ACL even in extensions.
--extensionacl "on-declarations"
# Explicit is better than implicit.
--disable redundantNilInit
# Indentation for multi-line string literals.
--indentstrings true

View File

@ -1 +0,0 @@
0.9.0

View File

@ -1,14 +0,0 @@
include = ["Cargo.toml", "rust/**/*.toml"]
[formatting]
align_comments = false
indent_string = ' '
reorder_keys = false
[[rule]]
include = ["**/Cargo.toml"]
keys = ["dependencies", "workspace.dependencies", "dev-dependencies", "build-dependencies"]
[rule.formatting]
inline_table_expand = false
reorder_keys = true

View File

@ -1 +0,0 @@
java openjdk-17.0.2

View File

@ -9,7 +9,7 @@ These should usually be prioritized in that order, but adjust the trade-off as n
# General
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintenance, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
- **The bridging layer is not API.** As noted in the [readme](README.md), the primary purpose of this library is to provide good Java, Swift, and TypeScript APIs. We also try to make the non-bridge crates have a nice API, both for our own maintainence, testing, and internal use; and for external users who want to use or fork our crate. However, the Rust APIs in rust/bridge/ and the raw C symbols / JNI entry points / Node module we build are not considered public-facing at all. Use that to keep everything else nice!
(Not that you should be sloppy in the bridging layer. Maintainability is still a priority!)
@ -21,21 +21,12 @@ These should usually be prioritized in that order, but adjust the trade-off as n
- **Every change should have tests** or be covered by existing tests. There are sometimes exceptions to this, but a lot of times the act of justifying the exception can suggest how to write the tests instead.
## Logging
- **Logs should not contain user data**, including the default stringification for errors (Rust's Display, Java and TypeScript's `toString()`, Swift's `description`). "Debug" and "verbose" log levels are an exception to this, since they are turned off at compile time in our client library release builds. Note that this isn't "any information that can uniquely distinguish one user from another" (an ephemeral public key can do that, and there are legitimate reasons to log those; use your best judgment), but it is "any information that includes user input" (such as unencrypted usernames), "any information that can be linked back to a Signal account" (such as identity keys), and of course "any passwords or private keys".
One place where this is particularly subtle is when working with types that come from dependencies, especially errors. If the dependency has access to any such potentially-sensitive information, it's best to assume it could make it into arbitrary output, including error messages. The libsignal-net crate is particularly sensitive to this and constrains its errors with a custom LogSafeDisplay trait, but this isn't perfect.
Low-level objects like ServiceId and ProtocolAddress do not follow this rule; instead, they stringify in fixed formats that are easy to filter from higher-level logs en masse.
- **Logs should be kept minimal on success paths**. It's harder to find significant information in a sea of "operation succeeded!", and in the worst case we'd hit the log size limit sooner. (Clients only keep a few days of logs, and they'll keep even less if the recent logs are taking up too much space.) Even on failure paths, consider how much will end up in client logs, and if it'll be redundant with a higher-level log. Especially when logging in a loop. But don't go too far: it's important to know when certain events happen in relation to earlier or later failures.
As with the previous rule, this does not apply to the "debug" and "verbose" log levels, which are turned off at compile time in our client library release builds.
- **Only use "error"-level logs for bugs**. The apps and our log-processing tools may highlight "error"-level logs specially (e.g. asking the user to submit a debug log), so something bad that can happen for benign reasons like "a network connection dropped" should only be a "warning", not an "error". This doesn't have to be perfect, e.g. an incoming message might not be decryptable because the local user has restored their desktop OS from a snapshot. Instead, take it as "in the absence of other information, would we investigate this event alone as a possible bug?"
# Rust
@ -45,30 +36,16 @@ These should usually be prioritized in that order, but adjust the trade-off as n
- **Prefer `expect()` to `unwrap()`.** As noted, we don't have a no-panics policy, but `expect()` forces you to write down why you believe something should *never* happen except for programmer errors. In particular, untrusted input that fails to validate should *not* panic.
As an exception, it's okay to use `unwrap()` in tests, though `expect()` is still preferred if it's for the thing you're actively testing.
(Yes, there's a Clippy lint for this, but we also have a lot of code that predates this guideline.)
- You don't have to write doc comments on everything, but **if you do write a comment, make it a doc comment**, because they show up more nicely in IDEs.
- We build with a pinned nightly toolchain, but **we also support stable**. The specific minimum supported version of stable is listed in our top-level Cargo.toml and checked in CI. We permit ourselves to bump this as needed, but try not to do so capriciously because we know external people might be in non-rustup scenarios where getting a new stable is tricky; in practice we often end up following tokio's "six months back" policy. If you need to bump the minimum supported version of stable, make sure the next release has a "breaking" version number.
- We build with a pinned nightly toolchain, but **we also support stable**. The specific minimum supported version of stable is checked in CI (specifically, at the top of [build_and_test.yml](.github/workflows/build_and_test.yml)). We permit ourselves to bump this as needed, but try not to do so capriciously because we know external people might be in non-rustup scenarios where getting a new stable is tricky. If you need to bump the minimum supported version of stable, make sure the next release has a "breaking" version number.
- Crate-level Cargo.tomls don't usually inherit the workspace `rust-version`, because many crates are relatively stable and may continue working for external folks using earlier versions of Rust even though we no longer test for them; picking up the top-level MSRV update would therefore be unnecessarily breaking. Instead, they have a `rust-version` that indicates a known minimum at some point in the past; it may be too low, but it will never be overly high. The exceptions are the `bridge` crates, which are not intended to be used for anything but the app language libraries.
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases). Unreleased changes are collected in [RELEASE_NOTES.md][], which is reset after each release.
- **Avoid `cargo add`**, or fix up the Cargo.toml afterwards. Some of our dependency lists are organized and `cargo add` doesn't respect that.
- **We do not have a changelog file**; we rely on [GitHub displaying all our releases](https://github.com/signalapp/libsignal/releases).
- We do not have consistent guidelines for how to do errors in Rust, and the different crates do them differently. :-(
- When profiling on an aarch64 device, you need to **explicitly enable hardware AES support** in the `aes` crate:
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}"
These are automatically detected on x86_64, but will require an opt-in for aarch64 until we can update to `aes 0.9` or newer (not out yet at the time of this writing). All our app library build scripts set this themselves, but doing a manual `cargo build --release` will not.
- Our bridging logic uses code generation tools for the app-language interface files (C header for Swift, wrapper APIs for Java/Kotlin and TypeScript). These tools, or the macros used with them, depend on how types are written in `#[bridge_fn]` and other bridged APIs. Therefore, **use qualified names for non-std, non-libsignal types** in bridged signatures, so that they can be matched specifically and without ambiguity.
(There is one exception: `uuid::Uuid` has been `Uuid` for a long time, and is sufficiently unique to justify leaving it that way.)
## Async
@ -78,14 +55,10 @@ These should usually be prioritized in that order, but adjust the trade-off as n
More background here: "[Why doesn't tokio::select! require FusedFuture?](https://users.rust-lang.org/t/why-doesnt-tokio-select-require-fusedfuture/46975)"
- When bridging async APIs that use `#[bridge_io]`, **remember that the arguments and results will cross thread/queue/actor boundaries**, even in Node where there's only one JavaScript thread. Most of the time Rust's own Send/Sync checking will prevent this from being a problem, but whatever types are passed across the bridge layer will be unchecked, and you, the author of the code, will have to think about whether it's a problem (on both sides of the bridge). Usually it won't be! Value types like C structs and immutable Java objects are fine, it's only mutable objects and raw pointers where you have to be careful.
Async APIs that do not use `#[bridge_io]` are always run on the calling thread: for Java and Swift, they are run to completion immediately, and for Node they are run by being scheduled on the JavaScript microtask queue. However, in theory any calls back into app code could still lead to reentrant use, and any borrowed Rust objects might be accessed from other threads while the operation is ongoing.
# Java
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 23, at the time of this update), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
- Many of our APIs are shared between Android and Server, and we also run the client tests on desktop machines, so **stick to Java 8** unless you've verified that something newer is available on Android (back to our earliest supported version, API level 21, at the time of this writing), and don't use Android-specific APIs unless you're actually in Android-specific code. (This *should* be checked in CI but things have slipped through before, and it'll save you time to know whether you're allowed to use something.)
- **Put server-specific APIs in the server/ folder if they're not needed to test client features**, so they don't add code size for Android.
@ -93,28 +66,16 @@ These should usually be prioritized in that order, but adjust the trade-off as n
- **Write javadocs** unless an API is trivial (or not app-team-facing). Even for internal methods, though, if you do write a comment, make it a doc comment (like for Rust code), because it shows up in IDEs.
- Our Java code gets minified with [Android's R8] tool, which scans for usages of all items (classes, methods, fields) and prunes those that are never used. It can't see usages from Rust code via JNI, so additional annotations are required. **Annotate classes, methods, and fields that are accessed via JNI with `@CalledFromNative`**, which is recognized by the directives in [`libsignal.pro`], to ensure they are kept.
[Android's R8]: https://developer.android.com/build/shrink-code
[`libsignal.pro`]: ./java/shared/resources/META-INF/proguard/libsignal.pro
# Swift
- We support back to **iOS 15** (at the time of this writing), so newer APIs may not be available. This will be checked on build, so you can't get it wrong.
- We support back to **iOS 13** (at the time of this writing), so newer APIs may not be available. This will be checked on build, so you can't get it wrong.
- **Write API docs** using [DocC syntax][] (a Markdown dialect), unless an API is trivial (or not app-team-facing). Even for internal methods, though, if you do write a comment, make it a doc comment (like for Rust code), because it shows up in IDEs.
- To make sure that error messages get into logs, we use the `failOnError` helper instead of `try!` for forcing an unwrap on the result of an operation that can throw an error.
- [`Sendable`][] is a part of Swift's concurrency-checking model similar to Rust's `Send` and `Sync`. In general, **any `public` struct or enum should be marked `Sendable`** unless it wraps something that isn't Sendable (or if it's an enum just used for namespacing). You don't have to do this for non-public structs and enums; Swift will infer whether they are Sendable within the library automatically.
Classes are trickier: a class that will forever be immutable is safe to mark `Sendable`, as is a class whose methods are designed to be called from multiple threads (often shortened to "this class is thread-safe"). However, unless you can make the class `final` *and* it doesn't have a superclass that's non-`Sendable`, the compiler won't be able to check it for you, and you'll have to write `@unchecked Sendable` instead. Be careful that this really is safe, and that we won't ever want to introduce mutating operations! It's easier to add Sendable later than to remove it, so err on the side of not including it.
As an approximation, `Sendable` in Swift is *roughly* equivalent to Rust's `Send` for value types (e.g. structs) and `Sync` for reference types (e.g. classes), because every reference in Swift has an implicit `Arc` around it.
[DocC syntax]: https://www.swift.org/documentation/docc/writing-symbol-documentation-in-your-source-files
[`Sendable`]: https://developer.apple.com/documentation/swift/sendable
# TypeScript

4621
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,33 +3,27 @@
members = [
"rust/attest",
"rust/crypto",
"rust/debug",
"rust/device-transfer",
"rust/keytrans",
"rust/media",
"rust/message-backup",
"rust/net",
"rust/net/chat",
"rust/net/infra",
"rust/account-keys",
"rust/pin",
"rust/poksho",
"rust/protocol",
"rust/svr3",
"rust/usernames",
"rust/zkcredential",
"rust/zkgroup",
"rust/bridge/ffi",
"rust/bridge/jni",
"rust/bridge/jni/impl",
"rust/bridge/jni/testing",
"rust/bridge/node",
"rust/bridge/node/native_ts",
]
default-members = [
"rust/crypto",
"rust/device-transfer",
"rust/media",
"rust/message-backup",
"rust/account-keys",
"rust/pin",
"rust/poksho",
"rust/protocol",
"rust/usernames",
@ -38,218 +32,10 @@ default-members = [
]
resolver = "2" # so that our dev-dependency features don't leak into products
[workspace.package]
version = "0.94.1"
authors = ["Signal Messenger LLC"]
license = "AGPL-3.0-only"
rust-version = "1.88"
[workspace.lints.clippy]
# Prefer TryFrom between integers unless truncation is desired.
# For converting between floats and integers, there may not be an alternative.
cast_possible_truncation = "warn"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [
'cfg(fuzzing)',
'cfg(tokio_unstable)',
] }
[workspace.dependencies]
# Our own crates, so that we don't have to depend on them by inter-crate paths
attest = { path = "rust/attest" }
device-transfer = { path = "rust/device-transfer" }
libsignal-account-keys = { path = "rust/account-keys" }
libsignal-cli-utils = { path = "rust/cli-utils" }
libsignal-core = { path = "rust/core" }
libsignal-debug = { path = "rust/debug" }
libsignal-keytrans = { path = "rust/keytrans" }
libsignal-message-backup = { path = "rust/message-backup" }
libsignal-net = { path = "rust/net" }
libsignal-net-chat = { path = "rust/net/chat" }
libsignal-net-grpc = { path = "rust/net/grpc" }
libsignal-node = { path = "rust/bridge/node" }
libsignal-protocol = { path = "rust/protocol" }
libsignal-svrb = { path = "rust/svrb" }
poksho = { path = "rust/poksho" }
signal-crypto = { path = "rust/crypto" }
signal-media = { path = "rust/media" }
usernames = { path = "rust/usernames" }
zkcredential = { path = "rust/zkcredential" }
zkgroup = { path = "rust/zkgroup" }
libsignal-bridge = { path = "rust/bridge/shared" }
libsignal-bridge-macros = { path = "rust/bridge/shared/macros" }
libsignal-bridge-testing = { path = "rust/bridge/shared/testing" }
libsignal-bridge-types = { path = "rust/bridge/shared/types" }
libsignal-jni-impl = { path = "rust/bridge/jni/impl" }
signal-neon-futures = { path = "rust/bridge/node/futures" }
# Our forks of some dependencies, accessible as xxx_signal so that usages of them are obvious in source code. Crates
# that want to use the real things can depend on those directly.
boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "boring", default-features = false }
curve25519-dalek-signal = { git = 'https://github.com/signalapp/curve25519-dalek', package = "curve25519-dalek", tag = 'signal-curve25519-4.1.3' }
spqr = { git = "https://github.com/signalapp/SparsePostQuantumRatchet.git", tag = "v1.5.1" }
tokio-boring-signal = { git = "https://github.com/signalapp/boring", tag = "signal-v5.0.2", package = "tokio-boring" }
aes = "0.8.3"
aes-gcm-siv = "0.11.1"
anyhow = "1.0.97"
arbitrary = "1.4.2"
argon2 = "0.5.0"
arrayvec = "0.7.4"
asn1 = "0.23.0"
assert_cmd = "2.0.13"
assert_matches = "1.5"
async-compression = "0.4.5"
async-trait = "0.1.79"
atomic-take = "1.1.0"
auto_enums = "0.8.7"
base64 = "0.22.1"
bincode = "1.3.2"
bitflags = "2.9"
bitstream-io = "1.10.0"
blake2 = "0.10.6"
boring = { version = "5.0", default-features = false }
boring-sys = { version = "5.0", default-features = false }
bytes = "1.11.1"
cbc = "0.1.2"
cfg-if = "1.0.0"
chacha20poly1305 = "0.10.1"
chrono = "0.4.42"
clap = "4.4.11"
clap-stdin = "0.8.0"
const-str = "1.0"
criterion = "0.5"
ctr = "0.9.2"
curve25519-dalek = "4.1.3"
data-encoding-macro = "0.1.18"
derive-where = "1.6.1"
derive_more = "2.0.0"
dir-test = "0.4.1"
displaydoc = "0.2.5"
ed25519-dalek = "2.1.0"
either = "1.13.0"
env_logger = "0.11.7"
flate2 = { version = "1.1.1", default-features = false }
futures = "0.3"
futures-util = "0.3"
ghash = "0.5.0"
heck = "0.5"
hex = "0.4.3"
hickory-proto = "0.26.1"
hkdf = "0.12"
hmac = "0.12.0"
hpke-rs = "0.6.1"
hpke-rs-crypto = "0.6.1"
http = "1.3.0"
http-body = "1.0.1"
http-body-util = "0.1.3"
hyper = "1.7"
hyper-util = "0.1.17"
indexmap = "2.7.0"
intmap = "3.1.2"
itertools = "0.14.0"
jni = "0.21"
libc = "0.2.186"
libcrux-ml-kem = { version = "0.0.8", default-features = false }
linkme = "0.3.33"
log = "0.4.21"
log-panics = "2.1.0"
macro_rules_attribute = "0.2.0"
mediasan-common = "0.5.3"
minidump = { version = "0.22.1", default-features = false }
minidump-processor = { version = "0.22.1", default-features = false }
minidump-unwind = { version = "0.22.1", default-features = false }
minijinja = "2.19.0"
mp4san = "0.5.3"
neon = { version = "1.1.0", default-features = false }
nonzero_ext = "0.3.0"
once_cell = "1.20.0"
partial-default = "0.1.0"
paste = "1.0.15"
pbjson = "0.9.0"
pbjson-build = "0.9.0"
pbjson-types = "0.9.0"
pin-project = "1.1.5"
pretty_assertions = "1.4.0"
proc-macro2 = "1.0.93"
proptest = "1.7"
proptest-arbitrary-interop = "0.1.0"
proptest-state-machine = "0.4"
prost = "0.14"
prost-build = "0.14"
prost-types = "0.14"
protobuf = "3.7.2"
protobuf-codegen = "3.7.2"
quote = "1.0.40"
rand = "0.9.4"
rand_chacha = "0.9"
rand_core = "0.9"
rangemap = "1.5.1"
rayon = "1.8.0"
rcgen = "0.14.0"
ref-cast = "1.0.25"
rustls = { version = "0.23.25", default-features = false }
rustls-platform-verifier = "0.5.1"
scopeguard = "1.0"
serde = "1.0.203"
serde_json = "1.0.45"
serde_json5 = "0.2.1"
serde_with = "3.1.0"
sha1 = "0.10"
sha2 = "0.10"
snow = { version = "0.10", default-features = false }
socks5-server = "0.10.1"
static_assertions = "1.1"
strum = "0.27.0"
subtle = "2.6"
syn = "2.0.98"
syn-mid = "0.6"
test-case = "3.3"
test-log = "0.2.16"
testing_logger = "0.1.1"
thiserror = "2.0.11"
tls-parser = "0.12.2"
tokio = "1.52.2"
tokio-socks = "0.5.2"
tokio-stream = "0.1.16"
tokio-tungstenite = "0.28.0"
tokio-util = "0.7.18"
tonic = { version = "0.14", default-features = false }
tonic-prost = "0.14"
tonic-prost-build = { version = "0.14", default-features = false }
tower-service = "0.3.3"
tungstenite = "0.28.0"
unicode-segmentation = "1.12.0"
url = "2.5.4"
uuid = "1.5"
visibility = "0.1.1"
warp = "0.4.2"
webpsan = { version = "0.5.3", default-features = false }
x25519-dalek = "2.0.0"
zerocopy = "0.8.33"
zeroize = "1.8.2"
[patch.crates-io]
# When building libsignal, just use our forks so we don't end up with two different versions of the libraries.
boring = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
boring-sys = { git = 'https://github.com/signalapp/boring', tag = "signal-v5.0.2" }
curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.3' }
# Use our fork of curve25519-dalek for zkgroup support.
curve25519-dalek = { git = 'https://github.com/signalapp/curve25519-dalek', tag = 'signal-curve25519-4.1.1' }
boring = { git = 'https://github.com/signalapp/boring', branch = 'libsignal' }
[profile.dev.package.argon2]
opt-level = 2 # libsignal-account-keys unit tests are too slow with an unoptimized argon2
[profile.release]
overflow-checks = true
[profile.release.package.curve25519-dalek]
overflow-checks = false
[profile.release.package.sha2]
overflow-checks = false
[profile.release.package.hmac]
overflow-checks = false
opt-level = 2 # signal-signal-pin unit tests are too slow with an unoptimized argon2

View File

@ -5,7 +5,7 @@
Pod::Spec.new do |s|
s.name = 'LibSignalClient'
s.version = '0.94.1'
s.version = '0.41.2'
s.summary = 'A Swift wrapper library for communicating with the Signal messaging service.'
s.homepage = 'https://github.com/signalapp/libsignal'
@ -14,16 +14,18 @@ Pod::Spec.new do |s|
s.source = { :git => 'https://github.com/signalapp/libsignal.git', :tag => "v#{s.version}" }
s.swift_version = '5'
s.platform = :ios, '15.0'
s.platform = :ios, '13.0'
s.dependency 'SignalCoreKit'
s.source_files = ['swift/Sources/**/*.swift', 'swift/Sources/**/*.m']
s.preserve_paths = [
'swift/Sources/SignalFfi',
'bin/fetch_archive.py',
'acknowledgments/acknowledgments-ios.plist',
'acknowledgments/acknowledgments.plist',
]
pod_target_xcconfig = {
s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '$(PODS_TARGET_SRCROOT)/swift/Sources/SignalFfi',
# Duplicate this here to make sure the search path is passed on to Swift dependencies.
'SWIFT_INCLUDE_PATHS' => '$(HEADER_SEARCH_PATHS)',
@ -42,7 +44,6 @@ Pod::Spec.new do |s|
'CARGO_BUILD_TARGET[sdk=iphonesimulator*][arch=arm64]' => 'aarch64-apple-ios-sim',
'CARGO_BUILD_TARGET[sdk=iphonesimulator*][arch=*]' => 'x86_64-apple-ios',
'CARGO_BUILD_TARGET[sdk=iphoneos*][arch=arm64e]' => 'arm64e-apple-ios',
'CARGO_BUILD_TARGET[sdk=iphoneos*]' => 'aarch64-apple-ios',
# Presently, there's no special SDK or arch for maccatalyst,
# so we need to hackily use the "IS_MACCATALYST" build flag
@ -57,29 +58,10 @@ Pod::Spec.new do |s|
'ARCHS[sdk=iphonesimulator*]' => 'x86_64 arm64',
'ARCHS[sdk=iphoneos*]' => 'arm64',
}
user_target_xcconfig = {}
if ENV['LIBSIGNAL_TESTING_ONLY_ACTIVE_ARCH']
pod_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
user_target_xcconfig['ONLY_ACTIVE_ARCH'] = 'YES'
end
# This pod does not currently support explicit modules, but clients should specify that explicitly.
# `pod lib lint` doesn't provide that opportunity though.
if ENV['LIBSIGNAL_TESTING_DISABLE_EXPLICIT_MODULES']
user_target_xcconfig['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'NO'
end
s.pod_target_xcconfig = pod_target_xcconfig
s.user_target_xcconfig = user_target_xcconfig
s.script_phases = [
{ name: 'Download libsignal-ffi if not in cache',
{ name: 'Download and cache libsignal-ffi',
execution_position: :before_compile,
# It's not *ideal* to check the cache every build, but it's usually just a shasum.
# It might be possible to rely on the relative mtimes of the podspec and the fetched archive,
# but I wouldn't want to risk a mismatched archive giving us cryptic errors at link or run
# time later. This Is Fine.
always_out_of_date: '1',
script: %q(
set -euo pipefail
if [ -e "${PODS_TARGET_SRCROOT}/swift/build_ffi.sh" ]; then
@ -98,7 +80,7 @@ Pod::Spec.new do |s|
rm -rf "${LIBSIGNAL_FFI_TEMP_DIR}"
if [ -e "${PODS_TARGET_SRCROOT}/swift/build_ffi.sh" ]; then
# Local development
ln -fns "${PODS_TARGET_SRCROOT}" "${LIBSIGNAL_FFI_TEMP_DIR}"
ln -fhs "${PODS_TARGET_SRCROOT}" "${LIBSIGNAL_FFI_TEMP_DIR}"
elif [ -e "${SCRIPT_INPUT_FILE_0}" ]; then
mkdir -p "${LIBSIGNAL_FFI_TEMP_DIR}"
cd "${LIBSIGNAL_FFI_TEMP_DIR}"
@ -116,16 +98,9 @@ Pod::Spec.new do |s|
test_spec.preserve_paths = [
'swift/Tests/*/Resources',
]
test_pod_target_xcconfig = {
test_spec.pod_target_xcconfig = {
# Don't also link into the test target.
'LIBSIGNAL_FFI_LIB_TO_LINK' => '',
}
test_spec.pod_target_xcconfig = test_pod_target_xcconfig
# Ideally we'd do this at run time, not configuration time, but CocoaPods doesn't make that easy.
# This is good enough.
test_spec.scheme = {
environment_variables: ENV.select { |name, value| name.start_with?('LIBSIGNAL_TESTING_') }
}
end
end

147
README.md
View File

@ -12,7 +12,7 @@ as a Java, Swift, or TypeScript library. The underlying implementations are writ
- zkgroup: Functionality for [zero-knowledge groups][] and related features available in Signal.
- zkcredential: An abstraction for the sort of zero-knowledge credentials used by zkgroup, based on the paper "[The Signal Private Group System][]" by Chase, Perrin, and Zaverucha.
- poksho: Utilities for implementing zero-knowledge proofs (such as those used by zkgroup); stands for "proof-of-knowledge, stateful-hash-object".
- account-keys: Functionality for consistently using [PINs][] as passwords in Signal's Secure Value Recovery system, as well as other account-wide key operations.
- pin: Functionality for consistently using [PINs][] as passwords in Signal's Secure Value Recovery system.
- usernames: Functionality for username generation, hashing, and proofs.
- media: Utilities for manipulating media.
@ -41,31 +41,14 @@ increases to the minimum supported tools versions.
# Building
### Toolchain Installation
To build anything in this repository you must have [Rust](https://rust-lang.org) installed, as well
as recent versions of Clang, libclang, [CMake](https://cmake.org), Make, protoc, Python (3.9+), and git.
#### Linux/Debian
To build anything in this repository you must have [Rust](https://rust-lang.org) installed,
as well as Clang, libclang, [CMake](https://cmake.org), Make, protoc, and git.
On a Debian-like system, you can get these extra dependencies through `apt`:
```shell
$ apt-get install clang libclang-dev cmake make protobuf-compiler libprotobuf-dev python3 git
$ apt-get install clang libclang-dev cmake make protobuf-compiler git
```
#### macOS
On macOS, we have a best-effort maintained script to set up the Rust toolchain you can run by:
```shell
$ bin/mac_setup.sh
```
## Rust
### First Build and Test
The build currently uses a specific version of the Rust nightly compiler, which
will be downloaded automatically by cargo. To build and test the basic protocol
libraries:
@ -77,53 +60,13 @@ $ cargo test
...
```
### Additional Rust Tools
The basic tools above should get you set up for most libsignal Rust development.
Eventually, you may find that you need some additional Rust tools like `cbindgen` to modify the bridges to the
client libraries or `taplo` for code formatting.
You should always install any Rust tools you need that may affect the build from cargo rather than from your system
package manager (e.g. `apt` or `brew`). Package managers sometimes contain outdated versions of these tools that can break
the build with incompatibility issues (especially cbindgen).
To install the main Rust extra dependencies matching the versions we use, you can run the following commands:
```shell
$ cargo +stable install --version "$(cat .cbindgen-version)" --locked cbindgen
$ cargo +stable install --version "$(cat acknowledgments/cargo-about-version)" --locked cargo-about
$ cargo +stable install --version "$(cat .taplo-cli-version)" --locked taplo-cli
$ cargo +stable install cargo-fuzz
```
## Java/Android
### Toolchain Setup / Configuration
To build for Android you must install several additional packages including a JDK,
the Android NDK/SDK, and add the Android targets to the Rust compiler, using
```rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android```
Our officially supported JDK version for Android builds is JDK 17, so be sure to install e.g. OpenJDK 17, and then point JAVA_HOME to it.
You can easily do this on macOS via:
```shell
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
```
On Linux, the way you do this varies by distribution. For Debian based distributions like Ubuntu, you can use:
```shell
sudo update-alternatives --config java
```
We also check-in a `.tools_version` file for use with runtime version managers.
### Building and Testing
To build the Java/Android ``jar`` and ``aar``, and run the tests:
```shell
@ -132,64 +75,41 @@ $ ./gradlew test
$ ./gradlew build # if you need AAR outputs
```
You can pass `-P debugLevelLogs` to Gradle to build without filtering out debug- and verbose-level
logs from Rust, and `-P jniTypeTagging` to enable additional checks in the Rust JNI bridging code.
Alternately, a build system using Docker is available:
```shell
$ cd java
$ make
$ make java_test
```
When exposing new APIs to Java, you will need to run `rust/bridge/jni/bin/gen_java_decl.py` in
addition to rebuilding. This requires installing the `cbindgen` Rust tool, as detailed above.
addition to rebuilding.
### Use as a library
### Maven Central
Signal publishes Java packages for its own use, under the names org.signal:libsignal-server,
org.signal:libsignal-client, and org.signal:libsignal-android. libsignal-client and libsignal-server
contain native libraries for Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS
(x86_64 and arm64). libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and
x86_64 Android. These are located in a Maven repository at
https://build-artifacts.signal.org/libraries/maven/; for use from Gradle, add the following to your
`repositories` block:
```
maven {
name = "SignalBuildArtifacts"
// The "uri()" part is only necessary for Kotlin Gradle; Groovy Gradle accepts a bare string here.
url = uri("https://build-artifacts.signal.org/libraries/maven/")
}
```
Older builds were published to [Maven Central](https://central.sonatype.org) instead.
Signal publishes Java packages on [Maven Central](https://central.sonatype.org) for its own use,
under the names org.signal:libsignal-server, org.signal:libsignal-client, and
org.signal:libsignal-android. libsignal-client and libsignal-server contain native libraries for
Debian-flavored x86_64 Linux as well as Windows (x86_64) and macOS (x86_64 and arm64).
libsignal-android contains native libraries for armeabi-v7a, arm64-v8a, x86, and x86_64 Android.
When building for Android you need *both* libsignal-android and libsignal-client, but the Windows
and macOS libraries in libsignal-client won't automatically be excluded from your final app. You can
explicitly exclude them using `packaging`:
explicitly exclude them using `packagingOptions`:
```
android {
// ...
packaging {
packagingOptions {
resources {
excludes += setOf("libsignal_jni*.dylib", "signal_jni*.dll")
exclude "libsignal_jni.dylib"
exclude "signal_jni.dll"
}
}
// ...
}
```
You can additionally exclude `libsignal_jni_testing.so` if you do not plan to use any of the APIs
intended for client testing.
### Testing a local build with Signal-Android
The Signal-Android gradle.properties file has a commented-out line to include libsignal as part of the build. Uncomment that and adjust the path; optionally, you can restrict the architectures you want to build for by adding `androidArchs=aarch64` to *libsignal's* gradle.properties. (The set of recognized architectures is in java/build_jni.sh.) If you're using an IDE, you'll need to re-import the Gradle structure at this point. When you're done, revert the changes to the Android app's gradle.properties and re-import once more.
Note that this does not import the *Rust* parts of the project into the IDE. Doing that in a multi-language IDE like IDEA is possible, but finicky; as of 2025 the most reliable way to do it is to open the Android project first, add the libsignal repo root directory as a Rust project second (only including the top-level directory), and only then make the changes to gradle.properties.
## Swift
@ -201,20 +121,19 @@ To learn about the Swift build process see [``swift/README.md``](swift/)
You'll need Node installed to build. If you have [nvm][], you can run `nvm use` to select an
appropriate version automatically.
We use `npm` as our package manager, and a Python script to control building the Rust library, accessible as `npm run build`.
We use [`yarn`](https://classic.yarnpkg.com/) as our package manager. The Rust library will automatically be built when you run `yarn install`.
```shell
$ cd node
$ nvm use
$ npm install
$ npm run build
$ npm run tsc
$ npm run test
$ yarn install
$ yarn tsc
$ yarn test
```
When testing changes locally, you can use `npm run build` to do an incremental rebuild of the Rust library. Alternately, `npm run build-with-debug-level-logs` will rebuild without filtering out debug- and verbose-level logs.
When testing changes locally, you can use `yarn build` to do an incremental rebuild of the Rust library.
When exposing new APIs to Node, you will need to run `just generate-node` in
When exposing new APIs to Node, you will need to run `rust/bridge/node/bin/gen_ts_decl.py` in
addition to rebuilding.
[nvm]: https://github.com/nvm-sh/nvm
@ -226,10 +145,6 @@ libraries for Windows, macOS, and Debian-flavored Linux. Both x64 and arm64 buil
all three platforms, but the arm64 builds for Windows and Linux are considered experimental, since
there are no official builds of Signal for those architectures.
### Testing a local build with Signal-Desktop
After running all the build commands above, adjust the `@signalapp/libsignal-client` dependency in the Desktop app's package.json to "link:path/to/libsignal/node" and run `pnpm install`. When you're done, revert the changes to package.json and run `pnpm install` again.
# Contributions
@ -244,22 +159,6 @@ the project.
Signing a [CLA (Contributor License Agreement)](https://signal.org/cla/) is required for all contributions.
## Code Formatting and Acknowledgments
You can run the styler on the entire project by running:
```shell
just format-all
```
You can run more extensive tests as well as linters and clippy by running:
```shell
just check-pre-commit
```
When making a PR that adjusts dependencies, you'll need to regenerate our acknowledgments files. See [``acknowledgments/README.md``](acknowledgments/).
# Legal things
## Cryptography Notice
@ -276,6 +175,6 @@ Administration Regulations, Section 740.13) for both object code and source code
## License
Copyright 2020-2026 Signal Messenger, LLC
Copyright 2020-2024 Signal Messenger, LLC
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html

View File

@ -1,91 +1,76 @@
# Making a libsignal release
## 1. Run bin/prepare_release.py
## 0. Make sure all CI tests are passing on the latest commit
We maintain a helper script, prepare_release.py, to automate most of the rote work involved in cutting a release.
Check GitHub to see if the latest commit has all tests passing, including the nightly "Slow Tests". If not, fix the tests before releasing! (You can run the Slow Tests manually under the repository Actions tab on GitHub.)
This script:
1. Automatically checks to ensure the Continuous Integration tests pass.
2. Tags the release commit with the appropriate annotated tag, with the version number as the name and the release notes as the comment.
3. Prepares the repository for the next version, by:
1. Recording the code size of the just-released version in the repository,
2. Clearing RELEASE_NOTES.md and preparing it for new release notes for the presumed next version,
3. Updating the version number references throughout the repository to match the presumed next version, and finally
4. commiting all these changes in a single commit.
All these steps can be done manually if desired/needed, but the script makes it easier, incentivizing more frequent releases.
## 2. Push the release commit to signalapp/libsignal on GitHub
Once you have tagged a release commit using the script, you should push it to GitHub as discussed below. After you have pushed the tag, you can then kick off the submission of that version to the package repositories.
#### Pushing to Multiple Remotes
If you need to push the multiple remotes, you must take care, as it is a little tricky to ensure each remote ends up in the desired end state.
#### Pushing Only the Release to a Remote
If you want to push just the newly cut release to a remote, you need to push the following items:
1. All commits up to and including the tagged commit that marks the release. (This commit should be `HEAD~1` after running the `./bin/prepare_release.py` script.)
2. You should fast forward the main branch ref on that remote to point to that same commit.
3. You should also push the tag marking the release you just cut.
Pushing all these items generally looks something like this:
```
git push <remote> HEAD~1:main <release tag, e.g. v0.x.y>
```
#### Pushing the Release and the Preparation Commit to a Remote
If you want to push both the release and the preparation commit that resets the repository state in anticipation of the next commit to a remote, so that e.g. you can continue working on the next release, you need to push the following items:
1. All commits up to and including the preparation commit, which should be `HEAD` on after running `./bin/prepare_release.py`.
2. You should fast-forward the main branch ref to point to that preparation commit.
3. You should also push the tag marking the release you just cut.
Pushing all these items should generally look like:
```
git push <remote> HEAD:main <release tag, e.g. v0.x.y>
```
## 3. Submit to package repositories as needed
### Android and Server: Maven
In the signalapp/libsignal repository on GitHub, run the "Release - Java" action on the tag you just made.
### Node: NPM
In the signalapp/libsignal repository on GitHub, run the "Release - NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
### iOS: Build Artifacts
In the signalapp/libsignal repository on GitHub, run the "Release - iOS" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.
## Appendix: Release Standards and Information
### Versioning Methodology
## 1. Update the library version
The first version component should always be 0, to indicate that Signal does not promise stability between releases of the library.
A change is "breaking" if it will require updates in any of the Signal client apps or server components, or in external Rust clients of libsignal-protocol, zkgroup, poksho, attest, device-transfer, or signal-crypto. If there are any breaking changes, increase the second version component and reset the third to 0. Otherwise, increase the third version component.
### Release Notes Formatting
As we work, we keep running release notes in RELEASE_NOTES.md.
The format of these release notes should generally look something like:
```
v0.x.y
- Bar: Added a fancy new feature
- Fixed a bug in the foo crate
- Android: Exposed baz to Java clients
bin/update_versions.py 0.x.y
cargo check --workspace --all-features # make sure Cargo.lock is updated
bin/regenerate_acknowledgments.sh # include the new version number in the acknowledgments
```
v0.x.y is the version of the release. The changes are then listed in arbitrary order. It's important that the tag comment also includes the version number as the first line, because GitHub formats it as a title.
## 2. Record the code size for the Java library
On GitHub, under the Java tests for the most recent commit, copy the code size computed in the "java/check_code_size.py" step into a new entry in java/code_size.json.
## 3. Commit the version change and tag with release notes
```
git commit -am 'Bump to version v0.x.y'
git tag -a v0.x.y
```
Take a look at a past release for examples of the format:
```
v0.8.3
- Fixed several issues running signal-crypto operations on 32-bit
platforms.
- Removed custom implementation of AES-GCM-SIV, AES, AES-CTR, and
GHash in favor of the implementations from RustCrypto. The interface
presented to Java, Swift, and TypeScript clients has not changed.
- Updated several Rust dependencies.
- Java: Exposed the tag size for Aes256GcmDecryption.
```
(You might think repeating the version number in the summary field is redundant, but GitHub shows it as a title.)
## 4. Push the version bump and tag to GitHub
Note that both the tag *and* the branch need to be pushed.
## 5. Tag signalapp/boring if needed
If the depended-on version of `boring` has changed (check Cargo.lock), tag the commit in the public [signalapp/boring][] repository.
```
# In the checkout for signalapp/boring
git tag -a libsignal-v0.x.y -m 'libsignal v0.x.y' BORING_COMMIT_HASH
git push origin libsignal-v0.x.y
```
[signalapp/boring]: https://github.com/signalapp/boring
## 6. Submit to package repositories as needed
### Android and Server: Sonatype
In the signalapp/libsignal repository on GitHub, run the "Upload Java libraries to Sonatype" action on the tag you just made. Then go to [Maven Central][] and wait for the build to show up (it can take up to an hour).
[Maven Central]: https://central.sonatype.com/artifact/org.signal/libsignal-client/versions
### Node: NPM
In the signalapp/libsignal repository on GitHub, run the "Publish to NPM" action on the tag you just made. Leave the "NPM Tag" as "latest".
### iOS: Build Artifacts
In the signalapp/libsignal repository on GitHub, run the "Build iOS Artifacts" action on the tag you just made. Share the resulting checksum with whoever will update the iOS app repository.

View File

@ -1,5 +0,0 @@
v0.94.1
- Add `grpc.BackupsAnonymousGetUploadForm` remote config, for both backup and backup media uploads. This is separate from the `grpc.AttachmentsGetUploadForm` config added previously, which applies to regular attachment uploads.
- keytrans: Add reset account data field functionality for all platforms.

View File

@ -8,7 +8,7 @@ For the most part, libsignal is tested using each language's usual testing infra
% ./gradlew client:test server:test android:connectedAndroidTest
# Node
% npm run build && npm run tsc && npm run test
% yarn build && yarn tsc && yarn test
# Swift
% ./build_ffi.sh --generate-ffi && swift test
@ -17,15 +17,6 @@ For the most part, libsignal is tested using each language's usual testing infra
However, sometimes there are some more interesting test configurations; those are documented here.
# Rust Benchmarks
- If you are testing on an ARM64 device (including Desktop), you should compile with `RUSTFLAGS="--cfg aes_armv8"` to enable hardware support in the `aes` crate.
- Similarly, although most tests are not very sensitive to the speed of SHA-2, you should also compile with `--features sha2/asm`. (`libsignal-message-backup` turns this on by default as a dev-dependency.) This will go away when we get to update to sha2 0.11.
All of these configuration options are normally set either at the bridge crate level or in the build scripts for each bridged platform, but they may not be set when running with plain `cargo bench`.
# Running cross-compiling Rust tests with custom runners
Rust allows running tests with cross-compiled targets, but normally that only works if your system supports executing the cross-compiled binary (like Intel targets on ARM64 macOS or Windows, or 32-bit targets on 64-bit Linux or Windows). However, by overriding the "runner" setting for a particular target, we can run cross-compiled tests as well.
@ -42,7 +33,7 @@ Rust allows running tests with cross-compiled targets, but normally that only wo
ANDROID_NDK_HOME=path/to/ndk
CARGO_PROFILE_TEST_STRIP=debuginfo # make the "push" step take less time
CARGO_PROFILE_BENCH_STRIP=debuginfo # same for benchmarks
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android23-clang
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=path/to/ndk/toolchains/llvm/prebuilt/YOUR_HOST_HERE/bin/aarch64-linux-android21-clang
CARGO_TARGET_AARCH64_LINUX_ANDROID_RUNNER=bin/adb-run-test # in the repo root
```

View File

@ -17,7 +17,10 @@ Then:
Apart from the projects in this very repo, there are a few other crates that unavoidably have "synthesized" licenses based on their Cargo manifests:
- cesu8: Very old crate whose repository contains a license file for the Rust project itself, rather than the crate.
- hpke-rs-\*: Uploaded without a license file, though a license is listed in the Cargo.toml for each crate.
- curve25519-dalek-derive: Uploaded without a license file, though a license is listed in the Cargo.toml. Not the same as the license of curve25519-dalek.
- half: Not actually synthesized! Their license file just matches the synthesized text perfectly. A bug in cargo-about, presumably.
- pqcrypto-\*: Uploaded without a license file, though a license is listed in the Cargo.toml for each crate. The Kyber implementations we use are released as [Public Domain][kyber], so no acknowledgment is necessary.
[cargo-about]: https://embarkstudios.github.io/cargo-about/
[clarify]: https://embarkstudios.github.io/cargo-about/cli/generate/config.html#the-clarify-field-optional
[kyber]: https://github.com/PQClean/PQClean/blob/round3/crypto_kem/kyber1024/clean/LICENSE

View File

@ -6,9 +6,8 @@ accepted = [
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"MPL-2.0",
"AGPL-3.0-only",
"Unicode-3.0",
"AGPL-3.0",
"OpenSSL",
"Unicode-DFS-2016",
]
@ -24,35 +23,26 @@ no-clearly-defined = true
# in a fraction of the time.
max-depth = 1
workarounds = [
"chrono",
"prost",
"ring",
"tonic",
# List every target we ship, just in case some dependencies are platform-gated.
targets = [
"aarch64-apple-darwin",
"aarch64-pc-windows-msvc",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"x86_64-pc-windows-msvc",
"x86_64-unknown-linux-gnu",
"aarch64-apple-ios",
"aarch64-linux-android",
"armv7-linux-androideabi",
"i686-linux-android",
"x86_64-linux-android",
]
# The async-compression crates are embedded in a larger repo
[async-compression.clarify]
license = "MIT"
[[async-compression.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
[compression-codecs.clarify]
license = "MIT"
[[compression-codecs.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
[compression-core.clarify]
license = "MIT"
[[compression-core.clarify.git]]
path = "LICENSE-MIT"
checksum = "88d1e3160df48926ad3310a8ec5699b502889565908f1be7e77cd21282c7a709"
workarounds = [
"chrono"
]
# Boring's main license isn't at the root of the repo
@ -69,7 +59,7 @@ checksum = "48e488ce333f8a1e86a68b2a1df454464037f1ff580b5bff926053c56dbadc2d"
# and the similar configuration for 'ring' in https://github.com/EmbarkStudios/cargo-about/blob/3bcd3380f606fd468b2836e04cdcf7997d1f3ff8/src/licenses/workarounds/ring.rs
[boring-sys.clarify]
license = "MIT AND Apache-2.0"
license = "MIT AND ISC AND OpenSSL"
[[boring-sys.clarify.files]]
# The MIT license of the Rust code
@ -78,10 +68,37 @@ license = "MIT"
checksum = "ad2e7bdef7c00b92eaf4f657a472c7d3f8b36aac3cdc270e65bb0c287eec0d4e"
[[boring-sys.clarify.files]]
# The Apache 2.0 license of BoringSSL
# The original OpenSSL license
path = "deps/boringssl/LICENSE"
license = "Apache-2.0"
checksum = "827c8d8fc207c2392794eef9e00fe246f9f61fdcc132556c275be3dd8c3cd97f"
license = "OpenSSL"
start = "/* ===================================================================="
end = "*/"
checksum = "53552a9b197cd0db29bd085d81253e67097eedd713706e8cd2a3cc6c29850ceb"
[[boring-sys.clarify.files]]
# The ISC license of the Google-written BoringSSL code
path = "deps/boringssl/LICENSE"
license = "ISC"
start = "/* Copyright (c) 2015, Google Inc."
end = "* CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */"
checksum = "19c779f8bbc141fa15c14e0a15aacaee2da917f7043af883c90cbef3cd6f4847"
[[boring-sys.clarify.files]]
# The MIT license of the BoringSSL code in third_party/fiat
path = "deps/boringssl/LICENSE"
license = "MIT"
start = "Copyright (c) 2015-2016 the fiat-crypto authors"
end = "SOFTWARE."
checksum = "7d5e1fb4bbd5e89a687f94c3d3826db50e26bd6f4ade136a025dc2080c5bdc85"
# const-str is embedded in a larger repo
[const-str.clarify]
license = "MIT"
[[const-str.clarify.git]]
path = "LICENSE"
checksum = "565aacda8f5ea53f937f867ed49a0ac7e6828b60b4568803185cd0a13297d4e4"
# Newer versions of convert_case have a LICENSE file, we'll use that one
@ -133,113 +150,44 @@ path = "LICENSE"
checksum = "d2c376e2d8ee747383aaf0d5b52997bd6aa04ab73720b6797edeb64fe90c05a3"
# The hax crates are embedded in a larger repo.
[hax-lib.clarify]
license = "Apache-2.0"
# half includes both its licenses as separate files
[half.clarify]
license = "MIT" # OR Apache-2.0, but we're using MIT
[[hax-lib.clarify.git]]
[[half.clarify.files]]
path = "LICENSES/MIT.txt"
checksum = "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0"
# http-body is embedded in a larger repo
[http-body.clarify]
license = "MIT"
[[http-body.clarify.git]]
path = "LICENSE"
checksum = "9a50bad5a51e0ad726ea3a7f4b7b758e1b4d1784e6abefe1367f5bf01e972725"
checksum = "0345e2b98685e3807fd802a2478085dcae35023e3da59b5a00f712504314d83a"
[hax-lib-macros.clarify]
license = "Apache-2.0"
[http-body-util.clarify]
license = "MIT"
[[hax-lib-macros.clarify.git]]
[[http-body-util.clarify.git]]
path = "LICENSE"
checksum = "9a50bad5a51e0ad726ea3a7f4b7b758e1b4d1784e6abefe1367f5bf01e972725"
checksum = "0345e2b98685e3807fd802a2478085dcae35023e3da59b5a00f712504314d83a"
# The libcrux crates are embedded in a larger repo.
[libcrux-hacl-rs.clarify]
license = "Apache-2.0"
# linkme-impl is embedded in a larger repo.
[linkme-impl.clarify]
license = "MIT OR Apache-2.0"
[[libcrux-hacl-rs.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-hkdf.clarify]
license = "Apache-2.0"
[[libcrux-hkdf.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-hmac.clarify]
license = "Apache-2.0"
[[libcrux-hmac.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-intrinsics.clarify]
license = "Apache-2.0"
[[libcrux-intrinsics.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-macros.clarify]
license = "Apache-2.0"
[[libcrux-macros.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-ml-kem.clarify]
license = "Apache-2.0"
[[libcrux-ml-kem.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-platform.clarify]
license = "Apache-2.0"
[[libcrux-platform.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-secrets.clarify]
license = "Apache-2.0"
[[libcrux-secrets.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-sha2.clarify]
license = "Apache-2.0"
[[libcrux-sha2.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-sha3.clarify]
license = "Apache-2.0"
[[libcrux-sha3.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[libcrux-traits.clarify]
license = "Apache-2.0"
[[libcrux-traits.clarify.git]]
path = "LICENSE"
checksum = "c517c468fc7f8d83319dd8b3743923f6891e0dfbaf7c57a874758c8f39b98564"
[[linkme-impl.clarify.git]]
path = "LICENSE-MIT"
checksum = "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3"
# miniz_oxide's LICENSE and LICENSE-MIT.md don't get consistently chosen between. Force the choice here.
[miniz_oxide.clarify]
license = "MIT OR Zlib OR Apache-2.0"
[[miniz_oxide.clarify.files]]
path = "LICENSE"
checksum = "4108245a1f2df9d4e94df8abed5b4ba0759bb2f9b40a6b939f1be141077ae50b"
# mp4san is embedded in a larger repo
# mp4san is embedded in a larger repo, and has a tag that doesn't match the revision in Cargo
[mediasan-common.clarify]
license = "MIT"
override-git-commit = "0.5.1"
[[mediasan-common.clarify.git]]
path = "LICENSE"
@ -247,6 +195,7 @@ checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
[mp4san.clarify]
license = "MIT"
override-git-commit = "0.5.1"
[[mp4san.clarify.git]]
path = "LICENSE"
@ -254,20 +203,13 @@ checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
[mp4san-derive.clarify]
license = "MIT"
override-git-commit = "0.5.1"
[[mp4san-derive.clarify.git]]
path = "LICENSE"
checksum = "f78d723e5d254b2037aa633b034dfe314caf37ace39727c66271b119027e5730"
[neon.clarify]
license = "MIT"
[[neon.clarify.git]]
path = "LICENSE-MIT"
checksum = "e47f19ffc3ed618c75d166781681b27c30f841f9b5b10fc488150b9128b19cac"
# partial-default-derive is embedded in a larger repo
[partial-default-derive.clarify]
license = "AGPL-3.0-only"
@ -277,62 +219,78 @@ path = "LICENSE"
checksum = "0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"
# protobuf-parse has an addendum for the standard Google protobufs
# procfs-core is embedded in a larger repo
[procfs-core.clarify]
license = "MIT"
[[procfs-core.clarify.git]]
path = "LICENSE-MIT"
checksum = "c5bbf39118b0639bf8bd391ae0d7d81f25c1cb4066e0fdae6a405b20fb7ca170"
# The prost-* crates are embedded in a larger repo.
[prost-build.clarify]
license = "Apache-2.0"
override-git-commit = "v0.9.0"
[[prost-build.clarify.git]]
path = "LICENSE"
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
[prost-derive.clarify]
license = "Apache-2.0"
override-git-commit = "v0.9.0"
[[prost-derive.clarify.git]]
path = "LICENSE"
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
[prost-types.clarify]
license = "Apache-2.0"
override-git-commit = "v0.9.0"
[[prost-types.clarify.git]]
path = "LICENSE"
checksum = "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2"
# The protobuf crates are embedded in a larger repo
[protobuf-parse.clarify]
license = "MIT"
[[protobuf-parse.clarify.files]]
[[protobuf-parse.clarify.git]]
path = "LICENSE.txt"
checksum = "ea240b0b1a772a073d2f8941f2145dd8f0b5b2d83c700107a84a1f7eb8ac7af1"
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
# rustls-platform-verifier-android is embedded in a larger repo
[rustls-platform-verifier-android.clarify]
license = "MIT OR Apache-2.0"
override-git-commit = "v/0.3.2"
[[rustls-platform-verifier-android.clarify.git]]
path = "LICENSE-MIT"
checksum = "1c7cf76689c837a68ed8d704994e52a0f2940c087958f860d17f3186afbdcc0c"
# ryu has an unusual choice of licenses
[ryu.clarify]
license = "Apache-2.0 OR BSL-1.0"
[[ryu.clarify.files]]
path = "LICENSE-APACHE"
checksum = "62c7a1e35f56406896d7aa7ca52d0cc0d272ac022b5d2796e7d6905db8a3636a"
# sync_wrapper's license isn't recognized for some reason
[sync_wrapper.clarify]
license = "Apache-2.0"
[[sync_wrapper.clarify.files]]
path = "LICENSE"
checksum = "0d542e0c8804e39aa7f37eb00da5a762149dc682d7829451287e11b938e94594"
# The tonic-prost crates are embedded in a larger repo
[tonic-prost.clarify]
[protobuf-support.clarify]
license = "MIT"
[[tonic-prost.clarify.git]]
path = "LICENSE"
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
[[protobuf-support.clarify.git]]
path = "LICENSE.txt"
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
[tonic-prost-build.clarify]
[protobuf-json-mapping.clarify]
license = "MIT"
[[tonic-prost-build.clarify.git]]
[[protobuf-json-mapping.clarify.git]]
path = "LICENSE.txt"
checksum = "97647e63047ef75a82ee2928b335df94f45c87e08777dc033393c73294f3a57a"
# tokio-macros is embedded in a larger repo
[tokio-macros.clarify]
license = "MIT"
[[tokio-macros.clarify.git]]
path = "LICENSE"
checksum = "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3"
checksum = "1a594f153f129c2de7b15f3262394bdca3dcc2da40058e3ea435c8473eb1f3a0"
# webpsan is embedded in a larger repo
# webpsan is embedded in a larger repo, and has a tag that doesn't match the revision in Cargo
[webpsan.clarify]
license = "MIT"
override-git-commit = "0.5.1"
[[webpsan.clarify.git]]
path = "LICENSE"
@ -347,41 +305,6 @@ license = "MIT"
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-implement.clarify]
license = "MIT"
[[windows-implement.clarify.files]]
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-interface.clarify]
license = "MIT"
[[windows-interface.clarify.files]]
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-link.clarify]
license = "MIT"
[[windows-link.clarify.files]]
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-result.clarify]
license = "MIT"
[[windows-result.clarify.files]]
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-strings.clarify]
license = "MIT"
[[windows-strings.clarify.files]]
path = "license-mit"
checksum = "c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383"
[windows-sys.clarify]
license = "MIT"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -59,7 +59,7 @@
<h4>Used by:</h4>
<ul class="license-used-by">
{{#each used_by}}
<li><a href="{{#if crate.repository~}} {{crate.repository}} {{~else~}} https://crates.io/crates/{{crate.name}} {{~/if}}">{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}</a></li>
<li><a href="{{#if crate.repository~}} {{crate.repository}} {{~else~}} https://crates.io/crates/{{crate.name}} {{~/if}}">{{crate.name}} {{crate.version}}</a></li>
{{/each}}
</ul>
<pre class="license-text">{{text}}</pre>

View File

@ -3,7 +3,7 @@
libsignal makes use of the following open source projects.
{{#each licenses}}
## {{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}{{/each}}
## {{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}} {{crate.version}}{{/each}}
```
{{{text}}}

View File

@ -19,7 +19,7 @@
<key>License</key>
<string>{{name}}</string>
<key>Title</key>
<string>{{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}}{{#if crate.source}} {{crate.version}}{{/if}}{{/each}}</string>
<string>{{#each used_by}}{{#unless @first}}, {{/unless}}{{crate.name}} {{crate.version}}{{/each}}</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>

View File

@ -1 +1 @@
0.8.2
0.6.0

View File

@ -1,20 +0,0 @@
#!/usr/bin/env bash
# Workaround for https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
# Invoke this like: ./bin/benchmark-criterion -p foo -- --save-baseline my_baseline
# Any arguments after the "--" will _only_ be passed to criterion binaries.
set -euo pipefail
SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"/..
declare -a CARGO_ARGS
while [[ "${1:-}" != "--" ]] && (! [[ -z "${1:-}" ]]); do
CARGO_ARGS+=("$1")
shift 1
done
if [[ "${1:-}" == "--" ]]; then
shift 1
fi
export LIBSIGNAL_BENCHMARK_ARGS="$@"
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
export CARGO_TARGET_AARCH64_APPLE_DARWIN_RUNNER="$PWD/bin/benchmark-criterion-helper.sh"
exec cargo bench "${CARGO_ARGS[@]}"

View File

@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Don't invoke this directly. See ./benchmark-criterion
# This script is intended to be invoked as a cargo runner.
# Add $LIBSIGNAL_BENCHMARK_ARGS to the arguments if the benchmark is criterion.
if "$@" --help 2>&1 | grep criterion > /dev/null; then
# We intentionally want to expand the args here
# shellcheck disable=SC2086
exec "$@" $LIBSIGNAL_BENCHMARK_ARGS
else
exec "$@"
fi

View File

@ -1,61 +0,0 @@
#!/usr/bin/env python3
#
# Copyright 2025 Signal Messenger, LLC
# SPDX-License-Identifier: AGPL-3.0-only
#
import os
import subprocess
import sys
from typing import Iterator
def rust_paths_to_remap() -> Iterator[str]:
# Repo root
yield os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
rust_sysroot = subprocess.check_output(['rustc', '--print', 'sysroot'], text=True).strip()
yield rust_sysroot
# Rust stdlib internals (must go after sysroot)
yield os.path.join(rust_sysroot, 'lib', 'rustlib', 'src', 'rust')
# There's a library/ folder inside rustlib/src/rust as well that's also redundant,
# but (a) there are precompiled strings with library/ as the root in the stdlib,
# and (b) both the stdlib and libsignal have a core/ subdirectory.
cargo_home = os.environ.get('CARGO_HOME', os.path.join(os.path.expanduser('~'), '.cargo'))
# Git dependencies
yield os.path.join(cargo_home, 'git', 'checkouts')
# Iterate over all crates.io dependency directories:
for index_dir in os.scandir(os.path.join(cargo_home, 'registry', 'src')):
if not index_dir.name.startswith('index.'):
continue
yield index_dir.path
def _main() -> int:
import argparse
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
# For invoking as `build_helpers.py print-rust-paths-to-remap`.
print_remap_parser = subparsers.add_parser('print-rust-paths-to-remap')
print_remap_parser.set_defaults(action='print-rust-paths-to-remap')
args = parser.parse_args(sys.argv[1:])
if 'action' not in args:
parser.print_usage(file=sys.stderr)
return 1
# This should be replaced with a `match` when we drop Python 3.9.
if args.action == 'print-rust-paths-to-remap':
for path in rust_paths_to_remap():
print(path)
return 0
else:
raise NotImplementedError(args.action)
if __name__ == '__main__':
sys.exit(_main())

View File

@ -6,8 +6,6 @@
# shellcheck shell=bash
check_rust() {
build_std="$1"
if ! command -v rustup > /dev/null && [[ -d ~/.cargo/bin ]]; then
# Try to find rustup in its default per-user install location.
# This will be important when running from inside Xcode,
@ -28,7 +26,7 @@ check_rust() {
fi
if [[ -n "${CARGO_BUILD_TARGET:-}" ]] && ! (rustup target list --installed | grep -q "${CARGO_BUILD_TARGET:-}"); then
if [[ -n "${build_std:-}" ]]; then
if [[ -n "${BUILD_STD:-}" ]]; then
echo "warning: Building using -Zbuild-std to support tier 3 target ${CARGO_BUILD_TARGET}." >&2
else
echo "error: Rust target ${CARGO_BUILD_TARGET} not installed" >&2
@ -39,38 +37,21 @@ check_rust() {
fi
}
# usage: copy_built_library target/release signal_jni out_dir/ signal_jni_amd64
# usage: copy_built_library target/release signal_node out_dir/libsignal_node.node
# copy_built_library target/release signal_jni out_dir/
copy_built_library() {
for pattern in "libX.dylib" "libX.so" "X.dll"; do
possible_library_name="${pattern%X*}${2}${pattern#*X}"
possible_augmented_name="${pattern%X*}${4}${pattern#*X}"
for possible_library_name in "lib$2.dylib" "lib$2.so" "$2.dll"; do
possible_library_path="$1/${possible_library_name}"
if [ -e "${possible_library_path}" ]; then
out_dir=$(dirname "$3"x) # trailing x to distinguish directories from files
echo_then_run mkdir -p "${out_dir}"
echo_then_run cp "${possible_library_path}" "$3/${possible_augmented_name}"
echo_then_run cp "${possible_library_path}" "$3"
break
fi
done
}
echo_then_run() {
for x in "$@"; do
# Put single quotes around any argument with spaces in it.
if [[ "$x" == *" "* ]]; then
echo -n "'$x' "
else
echo -n "$x "
fi
done
echo
echo "$@"
"$@"
}
rust_remap_path_options() {
python3 "$(dirname "${BASH_SOURCE[0]}")"/build_helpers.py print-rust-paths-to-remap |
while read -r prefix; do
# Echo everything on a single line, since it's going into an environment variable.
echo -n "--remap-path-prefix ${prefix}= "
done
}

View File

@ -1,12 +1,4 @@
#!/usr/bin/env -S bloaty -d crates -s vm -c
#
# Uses bloaty from https://github.com/google/bloaty. Run as
#
# ./crates_code_size.bloaty target/aarch64-linux-android/release/libsignal_jni.so -- baseline.so
#
# where baseline.so is the same file (here, libsignal_jni.so) built at the
# version to compare against.
#
#!/usr/bin/env bloaty -d crates -s vm -c
# We use VM size because otherwise the debug sections are included.
custom_data_source: {
@ -17,10 +9,6 @@ custom_data_source: {
pattern: "^(/rustc/|library/)"
replacement: "stdlib"
}
rewrite: {
pattern: "/boring-sys-[^/]+/out/boringssl/"
replacement: "BoringSSL"
}
rewrite: {
pattern: "/\\.?cargo/registry/src/(github.com|index.crates.io)-[^/]+/([^/]+)-\\d[^/]*/"
replacement: "\\2"
@ -30,7 +18,7 @@ custom_data_source: {
replacement: "\\1"
}
rewrite: {
pattern: "^(/?([^/]+/)+)src/"
pattern: "^(/?([^/]+/)*)src/"
replacement: "\\1"
}
rewrite: {

View File

@ -11,12 +11,12 @@
import argparse
import hashlib
import os
import ssl
import sys
import urllib.request
from typing import BinaryIO
UNVERIFIED_DOWNLOAD_NAME = 'unverified.tmp'
UNVERIFIED_DOWNLOAD_NAME = "unverified.tmp"
def build_argument_parser() -> argparse.ArgumentParser:
@ -36,40 +36,33 @@ def build_argument_parser() -> argparse.ArgumentParser:
def download_if_needed(archive_file: str, url: str, checksum: str) -> BinaryIO:
try:
fr = open(archive_file, 'rb')
f = open(archive_file, 'rb')
digest = hashlib.sha256()
chunk = fr.read1()
chunk = f.read1()
while chunk:
digest.update(chunk)
chunk = fr.read1()
chunk = f.read1()
if digest.hexdigest() == checksum.lower():
return fr
return f
print("existing file '{}' has non-matching checksum {}; re-downloading...".format(archive_file, digest.hexdigest()), file=sys.stderr)
except FileNotFoundError:
pass
print('downloading {}...'.format(archive_file), file=sys.stderr)
print("downloading {}...".format(archive_file), file=sys.stderr)
try:
with urllib.request.urlopen(url) as response:
digest = hashlib.sha256()
fw = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
f = open(UNVERIFIED_DOWNLOAD_NAME, 'w+b')
chunk = response.read1()
while chunk:
digest.update(chunk)
fw.write(chunk)
f.write(chunk)
chunk = response.read1()
assert digest.hexdigest() == checksum.lower(), 'expected {}, actual {}'.format(checksum.lower(), digest.hexdigest())
assert digest.hexdigest() == checksum.lower(), "expected {}, actual {}".format(checksum.lower(), digest.hexdigest())
os.replace(UNVERIFIED_DOWNLOAD_NAME, archive_file)
return fw
except (urllib.error.HTTPError, urllib.error.URLError) as e:
if isinstance(e.reason, ssl.SSLCertVerificationError):
# See:
#
# - https://stackoverflow.com/questions/27835619/urllib-and-ssl-certificate-verify-failed-error
# - https://stackoverflow.com/a/77491061
print('Failed to verify SSL certificate. Do you need to `pip install pip-system-certs`?', file=sys.stderr)
else:
print(e, e.filename, file=sys.stderr)
return f
except urllib.error.HTTPError as e:
print(e, e.filename, file=sys.stderr)
sys.exit(1)

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Install the latest Linux x86_64 protoc release using the GitHub CLI.
archive=$(mktemp)
trap 'rm -f "$archive"' EXIT
gh release download \
-R protocolbuffers/protobuf \
--pattern 'protoc-*-linux-x86_64.zip' \
--output "$archive" \
--clobber
# This extracts just bin/protoc and anything in the include directory
# to usr/local. We don't need anything else.
sudo unzip -q -o "$archive" -d /usr/local bin/protoc 'include/*'
echo "Installed protoc to /usr/local/bin/protoc"

View File

@ -1,37 +0,0 @@
#!/bin/bash
#set -ex
brew bundle install --file=- << EOF
brew "awscli"
brew "cmake"
brew "cocoapods"
brew "coreutils"
brew "fnm"
brew "gh"
brew "git"
brew "jq"
brew "just"
brew "pipx"
brew "protobuf"
brew "python"
brew "rocksdb"
brew "ruby"
brew "rustup"
brew "shellcheck"
brew "swiftlint"
brew "taplo"
brew "yamllint"
cask "gcloud-cli"
EOF
# Install Python tools using pipx.
# This keeps their dependencies isolated from other things on your system,
# but is still global state for each tool. We may some day want to switch this to a venv instead.
"$(brew --prefix pipx)/bin/pipx" install "mypy<2.0"
"$(brew --prefix pipx)/bin/pipx" install flake8
"$(brew --prefix pipx)/bin/pipx" inject flake8 \
flake8-comprehensions \
flake8-deprecated \
flake8-import-order \
flake8-quotes

View File

@ -1,506 +0,0 @@
#!/usr/bin/env python3
"""
This script automates the rote work to prepare for a release, as specified in RELEASE.md:
1) Checks that "Slow Tests" and "Build and Test" (CI) have succeeded on the current commit.
2) Creates a new annotated tag for the current commit, based on the release version found in RELEASE_NOTES.md.
3) Attempts to parse the Java code size from the GitHub Actions logs and appends that value (along with the version)
to java/code_size.json.
4) Resets RELEASE_NOTES.md to the next presumed version (e.g., incrementing PATCH).
5) Updates the version throughout the repository to that new version.
6) Commits these changes in a single "Reset for version X" commit.
Usage:
1) Ensure you are on the commit you wish to mark as a release
and that both Build and Test and Slow Tests have passed on that commit.
2) Run this script: ./prepare_release.py
3) Push the tag, the tag's commit, and the version reset/update commit to the proper remotes.
Optional arguments:
--skip-main-branch-check Skip the check that ensures we are on 'main' branch.
--skip-ci-tests-pass-check Skip the check that continous integration tests have passed on this commit.
--skip-worktree-clean-check Skip the check that the working tree is clean before running this script. Not recommended.
"""
import argparse
import json
import os
import re
import subprocess
import sys
import time
import traceback
from pathlib import Path
from shutil import which
class ReleaseFailedException(Exception):
pass
# Before we make any changes to the working tree/repository state, we add the command to rollback that change
# to this list. If we encounter an error, we execute these commands in order to return the repository to its
# original state.
# Each of these commands should be independent of each other, as if one of them fails, we will still try to
# execute the rest while performing a rollback.
on_failure_rollback_commands: list[list[str]] = []
# The following ids can be obtained by running:
# gh workflow list
# or, more programmatically friendly,
# gh workflow list --json id,name
BUILD_AND_TEST_WORKFLOW_ID = 6587503
SLOW_TEST_WORKFLOW_ID = 30989402
RELEASE_WORKFLOW_IDS = [
10143338, # Node
15104239, # Android
46287777, # iOS
]
def main() -> None:
parser = argparse.ArgumentParser(
description='Automates the release preparation workflow.'
)
parser.add_argument(
'--skip-main-branch-check',
action='store_true',
help="Skip the check to ensure the current branch is 'main'."
)
parser.add_argument(
'--skip-ci-tests-pass-check',
action='store_true',
help='Skip the check that continous integration tests have passed on this commit.'
)
parser.add_argument(
'--skip-worktree-clean-check',
action='store_true',
help='Skip the check that the working tree is clean before running this script. Not recommended.'
)
parser.add_argument(
'-n', '--dry-run',
action='store_true',
help='Skip any steps that would actually mutate the repository, for testing purposes.'
)
args = parser.parse_args()
try:
prepare_release(skip_main_check=args.skip_main_branch_check, skip_tests_pass_check=args.skip_ci_tests_pass_check, skip_worktree_clean_check=args.skip_worktree_clean_check, dry_run=args.dry_run)
exit_code = 0
except subprocess.CalledProcessError as e:
print(f'Error: command {e.cmd} exited with status {e.returncode}.')
exit_code = e.returncode
except ReleaseFailedException:
# We printed out the user friendly error before we threw the exception.
exit_code = 1
except KeyboardInterrupt:
print('User interrupted execution! Aborting...')
exit_code = 1
except Exception as ex:
traceback.print_exception(None, value=ex, tb=ex.__traceback__)
exit_code = 1
if exit_code != 0:
for rollback_command in on_failure_rollback_commands:
try:
run_command(rollback_command)
except subprocess.CalledProcessError:
rollback_command_str = ' '.join(rollback_command)
print(f'Unable to execute `{rollback_command_str}` after failure, working tree or repository may still be in dirty state.')
sys.exit(exit_code)
def get_workflow_name_mapping(repo_name: str) -> dict[int, str]:
"""Gets a mapping of workflow ids to their names from github."""
list_workflows_cmd = [
'gh', 'workflow', 'list',
'--repo', f'signalapp/{repo_name}',
'--json', 'name,id'
]
raw_json = run_command(list_workflows_cmd)
data = json.loads(raw_json)
return {d['id']: d['name'] for d in data}
def prepare_release(*, skip_main_check: bool = False, skip_tests_pass_check: bool = False, skip_worktree_clean_check: bool = False, dry_run: bool = False) -> None:
setup_and_check_env(skip_main_check, skip_worktree_clean_check)
REPO_NAME = get_repo_name()
RELEASE_NOTES_FILE_PATH = Path('RELEASE_NOTES.md')
# Obtain the workflow ids once
workflows = get_workflow_name_mapping(REPO_NAME)
# Get the commit sha of the commit we intend to mark as the release.
head_sha = run_command(['git', 'rev-parse', 'HEAD']).strip()
short_sha = head_sha[:9]
print(f'Searching for GitHub Actions runs for commit {short_sha}...')
# Release Step 1: Ensure that CI tests pass!
# - Check GitHub to see if the latest commit has all tests passing, including the nightly "Slow Tests".
# - If not, fix the tests before releasing!
# If needed, you can run the Slow Tests manually under the repository Actions tab on GitHub.
# You should run the Slow Tests before running this script.
if not skip_tests_pass_check:
build_and_test_run_id = check_workflow_success(REPO_NAME, workflows[BUILD_AND_TEST_WORKFLOW_ID], head_sha)
slow_test_run_id = check_workflow_success(REPO_NAME, workflows[SLOW_TEST_WORKFLOW_ID], head_sha)
print('Found GitHub Actions runs! They look good, but please double check manually as well.')
print(f'Build and Test: https://github.com/signalapp/{REPO_NAME}/actions/runs/{build_and_test_run_id}')
print(f'Slow Tests: https://github.com/signalapp/{REPO_NAME}/actions/runs/{slow_test_run_id}')
else:
print('Skipping checking that tests pass!')
print('Be sure to manually check for passing test runs at:')
print(f' https://github.com/signalapp/{REPO_NAME}/actions/workflows/build_and_test.yml')
print(f' https://github.com/signalapp/{REPO_NAME}/actions/workflows/slow_tests.yml')
# Release Step 2: Tag the release commit.
# - Look up the next version number vX.Y.Z according to our semantic versioning scheme, which
# is manually adjusted as needed in RELEASE_NOTES.md
# - Tag the release commit with an annotated tag titled with that version number and a message
# containing the release notes summarizing the notable changes since the last release from
# RELEASE_NOTES.md
# - Prompt the user to give the Release Notes a final human review. The expected format of the
# release notes is specified in RELEASE.md
head_release_version = tag_new_release(RELEASE_NOTES_FILE_PATH, dry_run=dry_run)
# Release Step 3: Prepare the repository for the next version
#
# Step 3, Stage 1: Update the version number throughout the repository to match the next presumed version
#
# We already have a script that does most of this, update_versions.py. We run it and pass the presumed next version
# number as an argument.
#
# We also run cargo check to make sure the version number in Cargo.lock is updated.
# We always start a release by presuming the next release will not be a breaking one. So, if the last release was v0.x.y, the next release
# is always presumed to be v0.x.(y+1) until a breaking change is merged.
major, minor, patch = parse_version(head_release_version)
next_patch = patch + 1
presumptive_next_version = f'v{major}.{minor}.{next_patch}'
if not skip_worktree_clean_check:
# Check again that the worktree is clean, just to be doubly sure we don't lose data.
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
on_failure_rollback_commands.append(['git', 'reset', '--hard'])
if not dry_run:
run_command(['./bin/update_versions.py', presumptive_next_version])
# Use subprocess.run() directly here to pass through `cargo check` output, because it may take a while.
subprocess.run(['cargo', 'check', '--workspace', '--all-features'], check=True)
# Step 3, Stage 2: Record the code size of the just cut release in code_size.json
# Get the cannonical computed code size for the Java library on the commit for the tagged release from GitHub
# Actions, and then add it to a new entry in java/code_size.json.
#
# The version for the new entry is the same as the version for the release that was just tagged, i.e. v0.x.y, not v0.x.(y+1).
# The "Build and Test" log contains the output of the 'java/check_code_size.py', which records the code size.
# So, we try to find the "Build and Test" log for this commit, but one may not exist.
# If it doesn't exist, we prompt the user to look it up manually.
if not skip_tests_pass_check:
print(f'Extracting Java library size from GitHub Actions run (ID: {build_and_test_run_id})...')
build_and_test_log = run_command([
'gh', 'run', 'view', str(build_and_test_run_id),
'--repo', f'signalapp/{REPO_NAME}',
'--log'
])
else:
build_and_test_log = ''
pattern = r'update code_size\.json with (\d+)' # Matches output of print_size_for_release in check_code_size.py
match = re.search(pattern, build_and_test_log)
if match:
java_code_size_int = int(match.group(1))
else:
print('Could not get logs to find Java code size automatically.')
print('This might be due to a known gh cli bug: https://github.com/cli/cli/issues/5011')
print(f"You'll have to find it manually in the list of runs: https://github.com/signalapp/{REPO_NAME}/actions/workflows/build_and_test.yml")
input_str = input('Please lookup the code size manually and enter it: ')
java_code_size_int = int(input_str)
code_size_file = Path('java/code_size.json')
append_code_size(code_size_file, head_release_version, java_code_size_int, dry_run=dry_run)
# Step 3, Stage 3: Clear RELEASE_NOTES.md, and update it with the presumptive next version number
#
# As we work, we keep updated running release notes for *just* the next release in RELEASE_NOTES.md. Because we just made a release that
# included all the changes previously in RELEASE_NOTES.md, it's now time to reset RELEASE_NOTES.md
#
# Thus, we edit RELEASE_NOTES.md so that it just contains the next version number on its own line, followed by one newline.
if not dry_run:
with RELEASE_NOTES_FILE_PATH.open('w', encoding='utf-8') as f:
f.write(presumptive_next_version + '\n\n')
# Step 3, Stage 4: Commit all changes in a single commit!
if not dry_run:
new_release_version = get_first_line_of_file(RELEASE_NOTES_FILE_PATH)
run_command([
'git', 'commit', '-am', f'Reset for version {new_release_version}'
])
upstream = os.environ.get('LIBSIGNAL_UPSTREAM_REMOTE') or '<remote>'
origin = os.environ.get('LIBSIGNAL_ORIGIN_REMOTE') or '<working-remote>'
print('\nRelease process complete!')
print('Next steps:')
print('1) Verify the GitHub Actions runs above passed.')
print('2) If they passed, push to the proper remote(s), e.g.:')
print(f'\tgit push {upstream} HEAD~1:main {head_release_version} && git push {origin} HEAD:main {head_release_version}')
print('3) To review the reset commit, you can run:')
print('\tgit show')
print('4) To run post-release actions, you can run:')
for id in RELEASE_WORKFLOW_IDS:
name = workflows[id]
raw_field = ' --raw-field dry_run=true' if dry_run else ''
print(f'\tgh workflow run "{name}" --repo signalapp/libsignal --ref {head_release_version}{raw_field}')
def setup_and_check_env(skip_main_check: bool = False, skip_worktree_clean_check: bool = False) -> None:
"""
Checks release environment pre-conditions.
Throws on failure.
"""
# We change into the repo root dir so we can use root-relative paths throughout
# the script. This matches the convention in other scripts, like update_versions.py.
repo_dir_path = run_command(['git', 'rev-parse', '--show-toplevel'])
os.chdir(repo_dir_path)
# We need to be authenticated with GitHub to fetch Actions run results from
# the API. We use these results to check that tests are passing, and to fetch
# the Java library code size from the Java test run logs.
# We opt to check this up front now and fail early, to try to minimize failures
# part way through the script that may leave the repository in a weird state.
check_gh_installed_and_authed()
# Optionally, we check to make sure we are on main as a convenience.
# Some people prefer instead to make this commit on a different branch, and
# then to 'git push <origin> HEAD:main', so we accomodate that with an opt-out.
if not skip_main_check:
current_branch = run_command(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).strip()
if current_branch != 'main':
print(f"Error: You are on branch '{current_branch}'.")
print("Please switch to 'main' or add the '--skip-main-branch-check' flag and then try again.")
raise ReleaseFailedException
if not skip_worktree_clean_check:
try:
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
except subprocess.CalledProcessError:
print('Error: Git working tree is not clean! This can cause unexpected behavior, as this script commits to Git.')
print('Please stash or commit your changes.')
print('You can also pass `--skip-worktree-clean-check` and try again to bypass this check, but this will result in')
print('any changes in your worktree being comitted to Git as part of the release, and is thus not recommended.')
raise ReleaseFailedException
if not skip_worktree_clean_check:
try:
run_command(['git', 'diff-index', '--quiet', 'HEAD', '--'])
except subprocess.CalledProcessError:
print('Error: Git working tree is not clean! This can cause unexpected behavior, as this script commits to Git.')
print('Please stash or commit your changes.')
print('You can also pass `--skip-worktree-clean-check` and try again to bypass this check, but this will result in')
print('any changes in your worktree being comitted to Git as part of the release, and is thus not recommended.')
sys.exit(1)
def tag_new_release(release_notes_file_path: Path, *, dry_run: bool) -> str:
if not release_notes_file_path.is_file():
print(f'Error: {release_notes_file_path} not found. Cannot proceed with release.')
raise ReleaseFailedException
# Read the top line of RELEASE_NOTES.md for the release version
head_release_version = get_first_line_of_file(release_notes_file_path)
if dry_run:
print(f'The release version is: {head_release_version}. [Normally the tag step would happen here.]')
return head_release_version
print('Opening an editor to create an annotated tag for this release.')
print('Please review and edit the release notes as needed.')
print('Once they look good, save and exit the editor to finalize the tag.\n')
time.sleep(5)
# Tag the release (and open an editor for the user)
# NB: We call subprocess.run() directly rather than run_command so we don't redirect stdin/stdout.
subprocess.run(
['git', 'tag', '--annotate', '--force', '--edit', head_release_version, '-F', str(release_notes_file_path)],
check=True
)
on_failure_rollback_commands.append(['git', 'tag', '-d', head_release_version])
print(f'Tagged new release: {head_release_version}')
return head_release_version
def get_repo_name() -> str:
# Some devs store the repo as "origin" remote, others store it as "private"
for remote in ('private', 'origin'):
try:
remote_url = run_command(['git', 'remote', 'get-url', remote], print_error=False).strip()
except subprocess.CalledProcessError:
continue
else:
break
else:
raise RuntimeError('Could not find a valid remote (origin or private).')
repo = remote_url.rsplit('/', 1)[-1]
if repo.endswith('.git'):
repo = repo[:-4]
return repo
def check_gh_installed_and_authed() -> None:
"""
Checks that the GitHub CLI ('gh') is installed and the user is authenticated.
Throws ReleaseFailedException if gh is not installed or authenticated.
"""
if which('gh') is None:
print('Error: GitHub CLI (gh) is not installed. Please install it and re-run.')
raise ReleaseFailedException
auth_status = subprocess.run(
['gh', 'auth', 'status'],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
if auth_status.returncode != 0:
print("You are not logged into GitHub CLI. Please run 'gh auth login' and re-run this script.")
raise ReleaseFailedException
def run_command(cmd: list[str], print_error: bool = True) -> str:
"""
Runs a shell command and returns its stdout as a string.
If check=True, raises a CalledProcessError for non-zero exit codes.
"""
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
if print_error:
print(f'Error while running command: {cmd}')
if e.stdout:
print('STDOUT:', e.stdout)
if e.stderr:
print('STDERR:', e.stderr)
raise
def check_workflow_success(repo_name: str, workflow_name: str, head_sha: str) -> int:
"""
Checks if a GitHub Actions workflow (workflow_name) has a run on HEAD (head_sha)
that completed successfully. Returns the run ID if found and successful;
otherwise prints an error and throws an exception.
"""
run_search_limit = '100'
list_cmd = [
'gh', 'run', 'list',
'--repo', f'signalapp/{repo_name}',
'--workflow', workflow_name,
'--limit', run_search_limit,
'--json', 'databaseId,headSha,status,conclusion'
]
raw_json = run_command(list_cmd)
runs_data = json.loads(raw_json)
matching_runs = [rd for rd in runs_data if rd['headSha'] == head_sha]
if not matching_runs:
print(f"Error: Could not find a matching '{workflow_name}' run for commit {head_sha}.")
print('Make sure CI has run successfully on the current commit before releasing.')
if workflow_name == 'Slow Tests':
print('Note that Slow Tests do not run automatically.')
print(f'You must kick them off automatically at: https://github.com/signalapp/{repo_name}/actions/workflows/slow_tests.yml')
print('Or by running')
print(f'\tgh workflow run "{workflow_name}" --repo signalapp/{repo_name} --ref main')
print('If tests have actually passed, you can skip this check by re-running with --skip-ci-tests-pass-check')
raise ReleaseFailedException
# Sort by run ID and pick the lowest
# NB: I opted to pick the lowest one, because as the first, it is less likely to be a re-run.
matching_runs.sort(key=lambda x: x['databaseId'])
selected_run_id = int(matching_runs[0]['databaseId'])
run_view_cmd = [
'gh', 'run', 'view', str(selected_run_id),
'--repo', f'signalapp/{repo_name}',
'--json', 'status,conclusion'
]
run_view_json = run_command(run_view_cmd)
try:
view_data = json.loads(run_view_json)
except json.JSONDecodeError:
print(f'Error: Could not parse JSON for run {selected_run_id}.')
raise ReleaseFailedException
status = view_data.get('status')
conclusion = view_data.get('conclusion')
if status != 'completed' or conclusion != 'success':
print(f"Error: '{workflow_name}' did not succeed (status={status}, conclusion={conclusion}).")
print('Please ensure all CI checks have passed before releasing.')
print('You can watch the run using:')
print(f'\tgh run watch {selected_run_id}')
raise ReleaseFailedException
return selected_run_id
def parse_version(version_str: str) -> tuple[int, int, int]:
"""
Given a string in the form 'vMAJOR.MINOR.PATCH',
returns (MAJOR, MINOR, PATCH) as integers.
"""
match = re.match(r'^v(\d+)\.(\d+)\.(\d+)$', version_str.strip())
if not match:
print(f"Error: version string '{version_str}' is not in 'vMAJOR.MINOR.PATCH' format.")
raise ValueError
major, minor, patch = match.groups()
assert int(major) == 0, 'Major version should always be zero, because we never promise stability to external users'
return int(major), int(minor), int(patch)
def get_first_line_of_file(filepath: Path) -> str:
"""
Returns the first line of the given file (stripped).
Throws on failure
"""
if not filepath.is_file():
print(f'Error: {filepath} not found.')
raise FileNotFoundError
with filepath.open('r', encoding='utf-8') as f:
return f.readline().strip()
def append_code_size(code_size_file: Path, version: str, code_size: int, *, dry_run: bool) -> None:
"""
Appends an object of the form { 'version': <version>, 'size': <code_size> }
to an existing JSON array in code_size_file.
Throws an exception if file not found or unable to load JSON.
"""
with code_size_file.open('r', encoding='utf-8') as f:
data = json.load(f)
new_entry = {'version': version, 'size': code_size}
data.append(new_entry)
if dry_run:
print(json.dumps(new_entry))
return
with code_size_file.open('w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
if __name__ == '__main__':
main()

View File

@ -11,24 +11,6 @@ SCRIPT_DIR=$(dirname "$0")
cd "${SCRIPT_DIR}"/..
. bin/build_helpers.sh
CHECK=0
case "${1:-}" in
--check)
CHECK=1
shift
;;
esac
if [ "$#" -ne 0 ]; then
echo "usage: $0 [--check]" >&2
exit 2
fi
if [ "$CHECK" -eq 1 ]; then
OUTPUT_DIR=$(mktemp -d)
trap 'rm -rf "$OUTPUT_DIR"' EXIT
fi
echo "Checking cargo-about version"
VERSION=$(cargo about --version)
echo "Found $VERSION"
@ -39,55 +21,6 @@ if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
false
fi
generate() {
template="$1"
output="$2"
shift 2
echo_then_run cargo about generate \
--config acknowledgments/about.toml \
--all-features --fail \
"$template" --output-file "$output" \
"$@"
}
generate_and_maybe_check() {
template="$1"
tracked_output="$2"
shift 2
if [ "$CHECK" -eq 1 ]; then
generated_output="${OUTPUT_DIR}/$(basename "$tracked_output")"
generate "$template" "$generated_output" "$@"
diff -u "$tracked_output" "$generated_output"
else
generate "$template" "$tracked_output" "$@"
fi
}
# List every target we ship, just in case some dependencies are platform-gated.
ANDROID_TARGETS=(
aarch64-linux-android
armv7-linux-androideabi
i686-linux-android
x86_64-linux-android
)
DESKTOP_TARGETS=(
aarch64-apple-darwin
aarch64-pc-windows-msvc
aarch64-unknown-linux-gnu
x86_64-apple-darwin
x86_64-pc-windows-msvc
x86_64-unknown-linux-gnu
)
IOS_TARGETS=(aarch64-apple-ios)
# shellcheck disable=SC2068 # We want "--target" to end up as a separate argument.
generate_and_maybe_check acknowledgments/acknowledgments{.html.hbs,.html} ${DESKTOP_TARGETS[@]/#/--target } ${IOS_TARGETS[@]/#/--target } ${ANDROID_TARGETS[@]/#/--target } --workspace
# shellcheck disable=SC2068
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/Cargo.toml
# shellcheck disable=SC2068
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-android-testing.md} ${ANDROID_TARGETS[@]/#/--target } --manifest-path rust/bridge/jni/testing/Cargo.toml
# shellcheck disable=SC2068
generate_and_maybe_check acknowledgments/acknowledgments{.md.hbs,-desktop.md} ${DESKTOP_TARGETS[@]/#/--target } --manifest-path rust/bridge/node/Cargo.toml
# shellcheck disable=SC2068
generate_and_maybe_check acknowledgments/acknowledgments{.plist.hbs,-ios.plist} ${IOS_TARGETS[@]/#/--target } --manifest-path rust/bridge/ffi/Cargo.toml
for template in acknowledgments/*.hbs; do
echo_then_run cargo about generate --config acknowledgments/about.toml --all-features --fail "$template" --output-file "${template%.hbs}"
done

View File

@ -1,33 +0,0 @@
#!/bin/bash
set -euo pipefail
# Script to run commands in a network-isolated namespace
# Usage:
# ./run_with_network_isolation.sh [command...]
# ./run_with_network_isolation.sh bash # interactive shell
#
# If no command is provided, defaults to bash
if [[ "$(uname -s)" != "Linux" ]]; then
echo "Error: This script uses network namespaces, and so it only works on Linux." >&2
exit 1
fi
RUN_UID=$(id -u)
RUN_GID=$(id -g)
ORIG_PATH="$PATH"
if [ $# -eq 0 ]; then
# No arguments, default to bash interactive shell
CMD="bash"
else
# Multiple arguments, join as command string
CMD="$*"
fi
DEESCALATE_AND_RUN_CMD="setpriv --reuid=${RUN_UID} --regid=${RUN_GID} --clear-groups -- bash -c \"${CMD}\""
SETUP_NETWORKING="ip link set lo up"
# Enter a network-isolated namespace as root, set up loopback, then run the command as the original user
# We have to pass PATH separetely to the de-escalated environment because it is stripped by sudo for safety.
sudo -E env PATH="$ORIG_PATH" unshare --net -- bash -c "${SETUP_NETWORKING} && ${DEESCALATE_AND_RUN_CMD}"

View File

@ -7,80 +7,69 @@
# Keep crate versions and lib package versions in accord
import collections
import fileinput
import os
import re
import subprocess
import sys
import re
import os
def read_version(file: str, pattern: re.Pattern[str]) -> str:
def read_version(file, pattern):
with open(file) as f:
for line in f:
match = pattern.match(line)
if match:
return match.group(2)
raise Exception(f'Could not determine version from {file}')
raise Exception(f"Could not determine version from {file}")
def update_version(file: str, pattern: re.Pattern[str], new_version: str) -> None:
def update_version(file, pattern, new_version):
with fileinput.input(files=(file,), inplace=True) as f:
for line in f:
print(pattern.sub(f'\\g<1>{new_version}\\g<3>', line, count=1), end='')
print(pattern.sub(f"\\g<1>{new_version}\\g<3>", line, count=1), end='')
# Note that all of these capture three groups; update_version() relies on that.
PODSPEC_PATTERN = re.compile(r"^(.*\.version\s+=\s+')(.*)(')")
GRADLE_PATTERN = re.compile(r'^(\s+version\s+=\s+")(.*)(")')
NODE_PATTERN = re.compile(r'^(\s+"version": ")(.*)(")')
CARGO_PATTERN = re.compile(r'^(version = ")(.*)(")')
RUST_PATTERN = re.compile(r'^(pub const VERSION: &str = ")(.*)(")')
RELEASE_NOTES_PATTERN = re.compile(r'^(v)(.*)()$')
def bridge_path(*bridge: str) -> str:
return os.path.join('rust', 'bridge', *bridge, 'Cargo.toml')
def bridge_path(bridge):
return os.path.join('rust', 'bridge', bridge, 'Cargo.toml')
VERSION_FILES = [
('RELEASE_NOTES.md', RELEASE_NOTES_PATTERN),
('LibSignalClient.podspec', PODSPEC_PATTERN),
(os.path.join('java', 'build.gradle'), GRADLE_PATTERN),
(os.path.join('node', 'package.json'), NODE_PATTERN),
(os.path.join('rust', 'core', 'src', 'version.rs'), RUST_PATTERN),
('Cargo.toml', CARGO_PATTERN),
]
def main() -> int:
def main():
os.chdir(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
if len(sys.argv) > 1:
new_version = sys.argv[1]
if new_version[0] == 'v':
new_version = new_version[1:]
for (path, pattern) in VERSION_FILES:
update_version(path, pattern, new_version)
# It's hard to update the package-lock.json file in a straightforward way with regexes, so use the appropriate
# tool.
subprocess.run(['npm', 'install', '--package-lock-only'], cwd='node')
update_version('LibSignalClient.podspec', PODSPEC_PATTERN, new_version)
update_version(os.path.join('java', 'build.gradle'), GRADLE_PATTERN, new_version)
update_version(os.path.join('node', 'package.json'), NODE_PATTERN, new_version)
update_version(bridge_path('ffi'), CARGO_PATTERN, new_version)
update_version(bridge_path('jni'), CARGO_PATTERN, new_version)
update_version(bridge_path('node'), CARGO_PATTERN, new_version)
return 0
found_versions = collections.defaultdict(list)
for (path, pattern) in VERSION_FILES:
version = read_version(path, pattern)
found_versions[version].append(path)
package_versions = {
'swift': read_version('LibSignalClient.podspec', PODSPEC_PATTERN),
'java': read_version(os.path.join('java', 'build.gradle'), GRADLE_PATTERN),
'node': read_version(os.path.join('node', 'package.json'), NODE_PATTERN)
}
if len(found_versions) != 1:
print('ERROR: found inconsistent versions:')
for (version, files) in sorted(found_versions.items()):
print(f'{version}:')
for file in files:
print(f' {file}')
bridge_versions = {
'swift': read_version(bridge_path('ffi'), CARGO_PATTERN),
'java': read_version(bridge_path('jni'), CARGO_PATTERN),
'node': read_version(bridge_path('node'), CARGO_PATTERN),
}
return 1
for bridge in package_versions:
if bridge_versions[bridge] != package_versions[bridge]:
print("ERROR: Bridge %s has package version %s but crate version is %s" % (
bridge, package_versions[bridge], bridge_versions[bridge]))
return 1
return 0

View File

@ -11,38 +11,22 @@
# You can use the `cargo tree` command below to see where they come from,
# and then document them here.
#
# thiserror: minimal and highly inlinable, most of the code is synthesized at the use site
# rand_core, getrandom: waiting to update all the RustCrypto crates together
# pqcrypto-kyber: v0.7 is what we shipped PQXDH on, v0.8 contains the NIST standard version
EXPECTED="
getrandom v0.2.16
getrandom v0.3.4
rand_core v0.6.4
rand_core v0.9.3
thiserror v1.0.69
thiserror v2.0.17"
pqcrypto-kyber v0.7.9
pqcrypto-kyber v0.8.0"
check_cargo_tree() {
# Only check the mobile targets, where we care most about code size.
cargo tree \
-p libsignal-node -p libsignal-jni -p libsignal-ffi \
--quiet --duplicates --edges normal,no-proc-macro \
--all-features --locked \
cargo tree --quiet --duplicates --edges normal,no-proc-macro \
--workspace --all-features --locked \
--target aarch64-apple-ios \
--target armv7-linux-androideabi \
--target aarch64-linux-android \
"$@"
}
ACTUAL="$(check_cargo_tree --depth 0 | sort -u -V)"
if [[ "${ACTUAL}" != "${EXPECTED}" ]]; then
cat <<EOF
----- EXPECTED -----
${EXPECTED}
------ ACTUAL ------
${ACTUAL}
EOF
if [[ "$(check_cargo_tree --depth 0 | sort -u -V)" != "${EXPECTED}" ]]; then
check_cargo_tree
exit 1
fi

View File

@ -1,7 +0,0 @@
#!/bin/bash
# This is expected to be the path to a BoringSSL *build* directory,
# but if we set it to the *source* directory it's enough for bindgen to run.
# And we can use a relative path because build scripts run from the package root).
export BORING_BSSL_PATH=deps/boringssl
command "$@"

1
doc/.gitignore vendored
View File

@ -1 +0,0 @@
book

View File

@ -1,4 +0,0 @@
[book]
language = "en"
multilingual = false
title = "libsignal"

View File

@ -1,24 +0,0 @@
# Introduction
libsignal contains platform-agnostic APIs used by the official Signal clients and servers, exposed as a Java, Swift, or TypeScript library. The underlying implementations are written in Rust.
This documentation is meant primarily for developers working at Signal who will be calling the libsignal APIs, and secondarily for those who maintain libsignal itself. It's meant to be a high-level guide to what's available, and generally won't contain implementation details or detailed API-by-API descriptions. It's *not* meant to be any sort of promise or commitment across versions of the library.
That is, if you're outside of Signal, please don't read too much into these.
## Viewing the book
First, [install mdBook](https://rust-lang.github.io/mdBook/guide/installation.html). Then, from the `doc` directory:
```console
% mdbook serve
2025-01-21 18:05:27 [INFO] (mdbook::book): Book building has started
2025-01-21 18:05:27 [INFO] (mdbook::book): Running the html backend
2025-01-21 18:05:27 [INFO] (mdbook::cmd::serve): Serving on: http://localhost:3000
2025-01-21 18:05:27 [INFO] (mdbook::cmd::watch::poller): Watching for changes...
2025-01-21 18:05:27 [INFO] (warp::server): Server::run; addr=[::1]:3000
2025-01-21 18:05:27 [INFO] (warp::server): listening on http://[::1]:3000
```
Now you can open the URL listed (probably <http://localhost:3000>) and view the rendered book. This is also a "watch" mode, which is convenient when editing the book---just save and watch the page reload.

View File

@ -1,8 +0,0 @@
# Summary
[Introduction](README.md)
- [Backups](backups/README.md)
- [Networking](net/README.md)
- [CDS]()
- [Chat](net/chat.md)

View File

@ -1,73 +0,0 @@
# Backups
libsignal has a handful of APIs related to backups:
- [Account keys](#account-keys)
- [Backup validation](#backup-validation)
- [BackupAuthCredential](#backupauthcredential)
## Account keys
Going forward, a number of account keys, including backup keys, will be derived from an *account entropy pool,* a 64-character alphanumeric ASCII string. While the AEP is represented as a plain String in libsignal APIs, methods to work with it can be found on `AccountEntropyPool`, including generation, validation, and derivation of other keys.
Derived from the account entropy pool is the `BackupKey`, a strongly-typed 32-byte blob that is used for all aspects of backups. There are many keys and identifiers that are derivable from a BackupKey.
Finally, the key specifically used to encrypt backup files is the `MessageBackupKey`, another strongly-typed object that consists of an HMAC key and an AES key for signing and encrypting the backup, respectively.
![](account-keys.svg)
## Backup validation
There are a few different APIs that work with backup files:
### MessageBackup: Bulk validation of an encrypted backup file stream
Takes in an encrypted input stream and produces validation results. Hard errors are thrown as exceptions, soft errors (unknown fields) are returned for manual checking or logging.
Validation makes two passes over the stream to verify its contained MAC both before and after parsing, so the relevant APIs take a callback to *produce* streams rather than a single stream object. There is no guarantee that the first stream has been fully consumed before the second is produced, so do not reuse the same stream object.
Provided by:
- `MessageBackup` in Java
- `validateMessageBackup` in Swift
- `validate` in the `MessageBackup` module in TypeScript
### OnlineBackupValidator: Validation of a backup as it's being made
Feed frames into the validator to check them one by one, but don't forget to finalize the backup (`close` in Java, `finalize` in Swift and TypeScript) to run the end-of-file checks!
This is usually going to be faster than the bulk validation because it skips the decryption and decompression steps, but of course this also means the encryption and compression of the backup being created aren't tested.
Only logs soft errors rather than returning them in a manipulatable form.
### ComparableBackup: "Canonicalization" of a backup for testing purposes
Takes an **unencrypted** backup as input and produces a JSON string as output, which can be formatted and diffed as JSON or just line-by-line. Don't forget to check for any unknown fields that get reported as well; they won't be included in the JSON and could invalidate any comparison.
Note that this format should not be considered stable (i.e. don't persist it or try to parse it). It's only intended for comparing two backups to each other; the usual way this is used is for an app to import Backup A and then immediately export its data as Backup B, then verify that the results are identical.
## BackupAuthCredential
The BackupAuthCredential types follow the usual zkgroup construction from the client's perspective, similar to the other AuthCredential variants:
```pseudocode
let requestContext = BackupAuthCredentialRequestContext.create(backupKey, aci)
let httpResponse = goRequestBackupAuthCredentialsFromServer(requestContext.getRequest())
for each response in httpResponse {
let expectedRedemptionTime = start of each day
let credential = requestContext.receive(response, expectedRedemptionTime, serverParams)
log(credential.backupLevel, credential.type)
goSaveCredential(credential, expectedRedemptionTime)
}
```
```pseudocode
let presentation = credential.present(serverParams)
goDoSomeBackupOperation(presentation)
```
More information on these credentials in the client/server docs.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 234 KiB

View File

@ -1,21 +0,0 @@
# Networking
libsignal has a number of networking-related APIs, collectively referred to as "libsignal-net". These currently provide connections to the [chat server][chat], contact discovery service, and SVR-B, with the possibility of eventually handling every connection to a server run by Signal.
## The Net class
**Net** (or **Network** on Android) is the top-level manager for connections made from libsignal. It records the environment (production or staging), the user agent to use for all connections (appending its own version string), and any configurable options that apply to all connections, such as whether IPv6 networking is enabled. Internally, it also owns a Rust-managed thread pool for dispatching I/O operations and processing responses. Some operations (e.g. CDS) are provided directly on Net; others use a separate connection object (e.g. chat) where the Net instance is merely used to connect.
## Implementation Organization
In the Rust layer, libsignal-net is broken up into three separate crates:
- `libsignal-net-infra`: Server- and connection-agnostic implementations of networking protocols
- `libsignal-net`: Connections specifically to Signal services, rather than generic reusable work
- `libsignal-net-chat`: Presents the high-level request APIs of the Signal chat server in a protocol-agnostic way (see the [Chat][] page for more info)
(These boundaries are approximate, because ultimately it's all going to be exposed to the apps anyway; these are *not* some of the crates designed to be generally reusable outside Signal.)
[chat]: ./chat.md

View File

@ -1,55 +0,0 @@
# Chat
Signal's chat server was historically been built on plain HTTP REST requests; to improve responsiveness for online clients, this was switched over to a pair of persistent WebSocket connections---one authenticated, one unauthenticated. To ease migration, these connections use an HTTP-like protobuf interface to provide the same API that REST used to, along with a dedicated "reverse request" mode for pushing incoming messages and notifying clients when the queue is empty. libsignal has two modes for working with chat connections: plain RPC, and "typed APIs".
## WebSocket RPC
To directly use this WebSocket RPC from libsignal, use the `connectAuth(enticated)Chat` or `connectUnauth(enticated)Chat` methods on a Net(work) instance. This produces an AuthenticatedChatConnection or an UnauthenticatedChatConnection, respectively, each of which has a `send()` method that can send a REST-like request. Note that for Android or iOS, the connection must be `start()`ed with a listener before it can be used.
The listener callbacks only cover disconnection and the two message-queue events, though these events are guaranteed to be delivered in order. They do not provide general-purpose server->client communication, even though the underlying protobuf interface would allow it.
### Preconnecting
If the time spent establishing a TLS connection becomes significant, Net also has a `preconnectChat()` call, which does the "early" part of connection establishment and then pauses, waiting for a later call to `connectAuthenticatedChat()`. This allows parallelizing the connection attempt with, say, loading the username and password used for the auth socket. This is considered an optimization; if `connectAuthenticatedChat()` isn't called soon after the initial `preconnectChat()` call, or if the connection parameters change in between, the preconnected socket will be silently discarded. If `connectAuthenticatedChat()` isn't called at *all,* the preconnected socket may not ever be cleaned up (but the server will eventually hang up on it).
## High-level Request APIs (a.k.a "Typed APIs")
To improve on the limitations of the current endpoints and the WebSocket RPC system, the chat server will support a new gRPC-based API that can replace the WebSocket RPC. Rather than have all clients bring up their own gRPC clients, libsignal will provide high-level equivalents for all the APIs currently using `send()` calls, and then transparently switch them to gRPC calls later. Using these high-level APIs differs slightly on each platform.
### Android
The typed APIs are provided as "services" that wrap the corresponding ChatConnection. For example:
```kotlin
val usernamesService = UnauthUsernamesService(chatConnection)
val response = usernamesService.lookUpUsernameHash(hash).get() // or await()
```
Unlike most libsignal APIs, which throw exceptions, the service APIs produce `RequestResult`s, a sealed interface of `Success` (what you wanted), `NonSuccess` (a request-specific error), and `Failure` (a standard transport error of some kind). This design was based on what Signal-Android was using elsewhere!
### iOS
The typed APIs are implemented directly on the corresponding ChatConnection, but also grouped into "service" protocols. Several hoops were jumped through to make it possible to access these in a generic way via the helper `UnauthServiceSelector` type (see there for more details).
```swift
// Assuming a helper accessService(_:as:) method added in the app.
try await accessService(chatConnection, as: .usernames) { usernamesService in
let response = try await usernamesService.lookUpUsernameHash(hash)
}
```
Each request can throw request-specific errors as well as standard transport errors.
### Desktop
The typed APIs are implemented directly on the corresponding ChatConnection, but also grouped into "service" interfaces. The libsignal tests contain an example of how to limit access in a generic way (see ServiceTestUtils.ts).
```typescript
// Assuming a helper accessUnauthService() method added in the app.
const service = connectionManager.accessUnauthService<UnauthUsernamesService>();
const response = await service.lookUpUsernameHash(hash);
```
Each request can throw (reject the promise) with request-specific errors as well as standard transport errors.

8
java/.gitignore vendored
View File

@ -1,8 +0,0 @@
# IntelliJ specific files/directories
out
.shelf
.idea
*.ipr
*.iws
*.iml
.kotlin

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
#
FROM ubuntu:jammy-20260109@sha256:c7eb020043d8fc2ae0793fb35a37bff1cf33f156d4d4b12ccc7f3ef8706c38b1
FROM ubuntu:jammy-20230624@sha256:b060fffe8e1561c9c3e6dea6db487b900100fc26830b9ea2ec966c151ab4c020
COPY java/docker/ docker/
COPY java/docker/apt.conf java/docker/sources.list /etc/apt/
@ -19,7 +19,7 @@ RUN apt-get update
# Install only what's needed to set up Rust and Android
# We'll install additional tools at the end to take advantage of Docker's caching of earlier steps.
RUN apt-get install -y openjdk-17-jdk-headless unzip
RUN apt-get install -y curl openjdk-17-jdk-headless unzip
ARG UID
ARG GID
@ -29,10 +29,9 @@ RUN groupadd -o -g "${GID}" libsignal \
&& useradd -m -o -u "${UID}" -g "${GID}" -s /bin/bash libsignal
USER libsignal
ENV HOME=/home/libsignal
ENV USER=libsignal
ENV SHELL=/bin/bash
ENV LANG=C.UTF-8
ENV HOME /home/libsignal
ENV USER libsignal
ENV SHELL /bin/bash
WORKDIR /home/libsignal
@ -41,43 +40,35 @@ ARG ANDROID_SDK_FILENAME=commandlinetools-linux-7583922_latest.zip
ARG ANDROID_SDK_SHA=124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e2fcfa7faf
ARG ANDROID_API_LEVELS=android-34
ARG ANDROID_BUILD_TOOLS_VERSION=34.0.0
ARG NDK_VERSION=28.0.13004108
ENV ANDROID_HOME=/home/libsignal/android-sdk
ENV PATH=${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/cmdline-tools/bin
ARG NDK_VERSION=25.2.9519653
ENV ANDROID_HOME /home/libsignal/android-sdk
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/cmdline-tools/bin
ADD --chown=libsignal --checksum=sha256:${ANDROID_SDK_SHA} \
https://dl.google.com/android/repository/${ANDROID_SDK_FILENAME} ${ANDROID_SDK_FILENAME}
RUN unzip -q ${ANDROID_SDK_FILENAME} -d android-sdk \
RUN curl -O https://dl.google.com/android/repository/${ANDROID_SDK_FILENAME} \
&& echo "${ANDROID_SDK_SHA} ${ANDROID_SDK_FILENAME}" | sha256sum -c - \
&& unzip -q ${ANDROID_SDK_FILENAME} -d android-sdk \
&& rm -rf ${ANDROID_SDK_FILENAME}
RUN yes | sdkmanager --sdk_root=${ANDROID_HOME} "platforms;${ANDROID_API_LEVELS}" "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" "platform-tools" "ndk;${NDK_VERSION}"
# Pre-download Gradle.
COPY java/gradle/wrapper gradle/wrapper
COPY java/gradle gradle
COPY java/gradlew gradlew
RUN ./gradlew --version
# Rust setup...
COPY rust-toolchain rust-toolchain
ARG RUSTUP_SHA=20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c
ARG RUSTUP_SHA=ad1f8b5199b3b9e231472ed7aa08d2e5d1d539198a15c5b1e53c746aad81d27b
ENV PATH="/home/libsignal/.cargo/bin:${PATH}"
ADD --chown=libsignal --chmod=755 --checksum=sha256:${RUSTUP_SHA} \
https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init /tmp/rustup-init
RUN /tmp/rustup-init -y --profile minimal --default-toolchain "$(cat rust-toolchain)" \
RUN curl -f https://static.rust-lang.org/rustup/archive/1.21.1/x86_64-unknown-linux-gnu/rustup-init -o /tmp/rustup-init \
&& echo "${RUSTUP_SHA} /tmp/rustup-init" | sha256sum -c - \
&& chmod a+x /tmp/rustup-init \
&& /tmp/rustup-init -y --profile minimal --default-toolchain "$(cat rust-toolchain)" \
&& rm -rf /tmp/rustup-init
RUN rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android aarch64-unknown-linux-gnu
# Manually install a newer protoc
ADD --chown=libsignal https://github.com/protocolbuffers/protobuf/releases/download/v29.3/protoc-29.3-linux-x86_64.zip protoc.zip
RUN unzip protoc.zip -d proto && rm -f protoc.zip
ENV PATH="/home/libsignal/proto/bin:${PATH}"
RUN rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
# Install the full set of tools now that the long setup steps are done.
# Note that we temporarily hop back to root to do this.
@ -87,12 +78,11 @@ USER root
RUN apt-get install -y \
clang \
cmake \
crossbuild-essential-arm64 \
git \
gpg-agent \
libclang-dev \
make \
python3
protobuf-compiler
USER libsignal
# Convert ssh to https for git dependency access without a key.

View File

@ -5,38 +5,38 @@
DOCKER ?= docker
.PHONY: docker java_build publish_java
.PHONY: docker java_build java_test publish_java
default: java_build
DOCKER_IMAGE := libsignal-builder
DOCKER_TTY_FLAG := $$(test -t 0 && echo -it)
DOCKER_PLATFORM ?= linux/amd64
GRADLE_OPTIONS ?= --dependency-verification strict
CROSS_COMPILE_SERVER ?= -PcrossCompileServer
docker_image:
cd .. && $(DOCKER) build --platform=$(DOCKER_PLATFORM) --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
cd .. && $(DOCKER) build --build-arg UID=$$(id -u) --build-arg GID=$$(id -g) -t $(DOCKER_IMAGE) -f java/Dockerfile .
java_build: DOCKER_EXTRA=$(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
java_build: docker_image
$(DOCKER) run $(DOCKER_TTY_FLAG) --init --rm --user $$(id -u):$$(id -g) \
--env LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS \
--env LIBSIGNAL_TESTING_IGNORE_KT_TESTS \
--env LIBSIGNAL_TESTING_PROXY_SERVER \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) build $(CROSS_COMPILE_SERVER)"
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
sh -c "cd src/java; ./gradlew build"
java_test: java_build
$(DOCKER) run $(DOCKER_TTY_FLAG) --init --rm --user $$(id -u):$$(id -g) \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) $(DOCKER_IMAGE) \
sh -c "cd src/java; ./gradlew test"
publish_java: DOCKER_EXTRA = $(shell [ -L build ] && P=$$(readlink build) && echo -v $$P/:$$P )
publish_java: docker_image
$(DOCKER) run --rm --user $$(id -u):$$(id -g) \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
-e ORG_GRADLE_PROJECT_sonatypeUsername \
-e ORG_GRADLE_PROJECT_sonatypePassword \
-e ORG_GRADLE_PROJECT_signingKeyId \
-e ORG_GRADLE_PROJECT_signingPassword \
-e ORG_GRADLE_PROJECT_signingKey \
-e CLOUDSDK_AUTH_ACCESS_TOKEN \
-v `cd .. && pwd`/:/home/libsignal/src $(DOCKER_EXTRA) \
$(DOCKER_IMAGE) \
sh -c "cd src/java; ./gradlew $(GRADLE_OPTIONS) publish $(CROSS_COMPILE_SERVER)"
sh -c "cd src/java; ./gradlew publish closeAndReleaseSonatypeStagingRepository"
# We could run these through Docker, but they would have the same result anyway.

View File

@ -1,5 +1,5 @@
plugins {
id 'com.android.library'
id 'com.android.library' version '8.3.0'
id 'androidx.benchmark' version '1.1.1'
}
@ -13,11 +13,9 @@ android {
compileSdk 34
defaultConfig {
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 33
multiDexEnabled = true
testInstrumentationRunner "androidx.benchmark.junit4.AndroidBenchmarkRunner"
multiDexEnabled true
// Uncomment this to build 32-bit-only benchmarks.
// (Gradle will still build a 64-bit libsignal,
@ -27,25 +25,21 @@ android {
// }
}
testBuildType = "release"
testBuildType "release"
compileOptions {
coreLibraryDesugaringEnabled = true
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace = "org.signal.libsignal.benchmarks"
packagingOptions {
doNotStrip '**/*.so'
}
namespace "org.signal.libsignal.benchmarks"
}
dependencies {
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.2.3'
androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.1.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
androidTestImplementation project(':android')
}

View File

@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:debuggable="false">
<profileable android:shell="true"/>
</application>
<application
android:debuggable="false"/>
</manifest>

View File

@ -1,66 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import java.time.Instant;
import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPni;
import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse;
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
public class ClientZkOperations {
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
private final Instant now = Instant.now();
private final ServerSecretParams serverParams = ServerSecretParams.generate();
private final ServerPublicParams serverPublicParams = serverParams.getPublicParams();
private final GroupSecretParams groupParams = GroupSecretParams.generate();
private final ServerZkAuthOperations serverZkAuthOperations =
new ServerZkAuthOperations(serverParams);
private final org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations clientZkOperations =
new org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations(serverPublicParams);
private final ServiceId.Aci aci = new ServiceId.Aci(UUID.randomUUID());
private final ServiceId.Pni pni = new ServiceId.Pni(UUID.randomUUID());
@Test
public void receiveAuthCredentialWithPni() throws VerificationFailedException {
final BenchmarkState state = benchmarkRule.getState();
state.pauseTiming();
final AuthCredentialWithPniResponse authCredentialWithPniResponse =
serverZkAuthOperations.issueAuthCredentialWithPniZkc(aci, pni, now);
state.resumeTiming();
while (state.keepRunning()) {
clientZkOperations.receiveAuthCredentialWithPniAsServiceId(
aci, pni, now.getEpochSecond(), authCredentialWithPniResponse);
}
}
@Test
public void createAuthCredentialPresentation() throws VerificationFailedException {
final BenchmarkState state = benchmarkRule.getState();
state.pauseTiming();
final AuthCredentialWithPniResponse authCredentialWithPniResponse =
serverZkAuthOperations.issueAuthCredentialWithPniZkc(aci, pni, now);
final AuthCredentialWithPni authCredentialWithPni =
clientZkOperations.receiveAuthCredentialWithPniAsServiceId(
aci, pni, now.getEpochSecond(), authCredentialWithPniResponse);
state.resumeTiming();
while (state.keepRunning()) {
clientZkOperations.createAuthCredentialPresentation(groupParams, authCredentialWithPni);
}
}
}

View File

@ -5,15 +5,18 @@
import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.signal.libsignal.protocol.ecc.*;
@RunWith(AndroidJUnit4.class)
public class ECCBenchmark {
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
private final ECKeyPair alicePair = ECKeyPair.generate();
private final ECKeyPair bobPair = ECKeyPair.generate();
private final ECKeyPair alicePair = Curve.generateKeyPair();
private final ECKeyPair bobPair = Curve.generateKeyPair();
private final byte[] arbitraryData = new byte[] {0x53, 0x69, 0x67, 0x6E, 0x61, 0x6C};
@Test

View File

@ -1,98 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.signal.libsignal.protocol.ServiceId;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
@RunWith(Parameterized.class)
public class GroupSendEndorsements {
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
@Parameters(name = "groupSize={0}")
public static Object[] data() {
return new Integer[] {10, 100, 1000};
}
private final ServerSecretParams serverParams = ServerSecretParams.generate();
private final ServerPublicParams serverPublicParams = serverParams.getPublicParams();
private final GroupSecretParams groupParams = GroupSecretParams.generate();
private final Instant expiration =
Instant.now().truncatedTo(ChronoUnit.DAYS).plus(2, ChronoUnit.DAYS);
private final ServiceId.Aci[] members;
private final UuidCiphertext[] encryptedMembers;
private final GroupSendEndorsementsResponse response;
public GroupSendEndorsements(int groupSize) {
members = new ServiceId.Aci[groupSize];
for (int i = 0; i < groupSize; ++i) {
members[i] = new ServiceId.Aci(UUID.randomUUID());
}
encryptedMembers = new UuidCiphertext[groupSize];
final ClientZkGroupCipher cipher = new ClientZkGroupCipher(groupParams);
for (int i = 0; i < groupSize; ++i) {
encryptedMembers[i] = cipher.encrypt(members[i]);
}
GroupSendDerivedKeyPair keyPair =
GroupSendDerivedKeyPair.forExpiration(expiration, serverParams);
response = GroupSendEndorsementsResponse.issue(Arrays.asList(encryptedMembers), keyPair);
}
@Test
public void benchmarkReceiveWithServiceIds() throws VerificationFailedException {
final BenchmarkState state = benchmarkRule.getState();
while (state.keepRunning()) {
response.receive(Arrays.asList(members), members[0], groupParams, serverPublicParams);
}
}
@Test
public void benchmarkReceiveWithCiphertexts() throws VerificationFailedException {
final BenchmarkState state = benchmarkRule.getState();
while (state.keepRunning()) {
response.receive(Arrays.asList(encryptedMembers), encryptedMembers[0], serverPublicParams);
}
}
@Test
public void benchmarkToToken() throws VerificationFailedException {
final BenchmarkState state = benchmarkRule.getState();
final List<GroupSendEndorsement> endorsements =
response
.receive(Arrays.asList(encryptedMembers), encryptedMembers[0], serverPublicParams)
.endorsements();
while (state.keepRunning()) {
for (GroupSendEndorsement next : endorsements) {
next.toToken(groupParams);
}
}
}
}

View File

@ -1,217 +0,0 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.signal.libsignal.metadata.SealedSessionCipher;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.libsignal.metadata.certificate.SenderCertificate;
import org.signal.libsignal.metadata.certificate.ServerCertificate;
import org.signal.libsignal.metadata.protocol.UnidentifiedSenderMessageContent;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.SessionBuilder;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.protocol.UntrustedIdentityException;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.groups.GroupCipher;
import org.signal.libsignal.protocol.groups.GroupSessionBuilder;
import org.signal.libsignal.protocol.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType;
import org.signal.libsignal.protocol.message.CiphertextMessage;
import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage;
import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
import org.signal.libsignal.protocol.state.PreKeyBundle;
import org.signal.libsignal.protocol.state.PreKeyRecord;
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
import org.signal.libsignal.protocol.state.impl.InMemorySignalProtocolStore;
@RunWith(Enclosed.class)
public class SealedSender {
public static class V1 {
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
@Test
public void sealedSenderV1Encrypt() throws Exception {
InMemorySignalProtocolStore aliceStore =
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xAA);
InMemorySignalProtocolStore bobStore =
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xBB);
SignalProtocolAddress bobAddress = new SignalProtocolAddress("+14152222222", 1);
SignalProtocolAddress aliceAddress =
new SignalProtocolAddress("9d0652a3-dcc3-4d11-975f-74d61598733f", 1);
initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress);
ECKeyPair trustRoot = ECKeyPair.generate();
SenderCertificate senderCertificate =
createCertificateFor(
trustRoot,
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
"+14151111111",
1,
aliceStore.getIdentityKeyPair().getPublicKey().getPublicKey(),
31337);
SealedSessionCipher aliceCipher =
new SealedSessionCipher(
aliceStore, UUID.fromString(aliceAddress.getName()), "+14151111111", 1);
final BenchmarkState state = benchmarkRule.getState();
while (state.keepRunning()) {
aliceCipher.encrypt(bobAddress, senderCertificate, "smert za smert".getBytes());
}
}
}
@RunWith(Parameterized.class)
public static class V2 {
@Parameterized.Parameters(name = "recipients={0}")
public static Object[] multiRecipientSizes() {
return new Integer[] {10, 100, 1000};
}
@Rule public final BenchmarkRule benchmarkRule = new BenchmarkRule();
final InMemorySignalProtocolStore aliceStore =
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), 0xAA);
final SignalProtocolAddress aliceAddress = new SignalProtocolAddress("+14151111111", 1);
final List<SignalProtocolAddress> recipients;
public V2(int recipientCount) {
recipients = new ArrayList<>(recipientCount);
for (int i = 0; i < recipientCount; ++i) {
InMemorySignalProtocolStore bobStore =
new InMemorySignalProtocolStore(IdentityKeyPair.generate(), i);
SignalProtocolAddress bobAddress =
new SignalProtocolAddress(UUID.randomUUID().toString(), i % 127 + 1);
filterExceptions(() -> initializeSessions(aliceStore, bobStore, bobAddress, aliceAddress));
recipients.add(bobAddress);
}
}
@Test
public void sealedSenderV2Encrypt() throws Exception {
GroupCipher aliceGroupCipher = new GroupCipher(aliceStore, aliceAddress);
UUID distributionId = UUID.randomUUID();
SenderKeyDistributionMessage sentAliceDistributionMessage =
new GroupSessionBuilder(aliceStore).create(aliceAddress, distributionId);
CiphertextMessage ciphertextFromAlice =
aliceGroupCipher.encrypt(distributionId, "smert ze smert".getBytes());
ECKeyPair trustRoot = ECKeyPair.generate();
SenderCertificate senderCertificate =
createCertificateFor(
trustRoot,
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
"+14151111111",
1,
aliceStore.getIdentityKeyPair().getPublicKey().getPublicKey(),
31337);
UnidentifiedSenderMessageContent content =
new UnidentifiedSenderMessageContent(
ciphertextFromAlice,
senderCertificate,
UnidentifiedSenderMessageContent.CONTENT_HINT_DEFAULT,
Optional.empty());
SealedSessionCipher aliceCipher =
new SealedSessionCipher(
aliceStore,
UUID.fromString("9d0652a3-dcc3-4d11-975f-74d61598733f"),
"+14151111111",
1);
final BenchmarkState state = benchmarkRule.getState();
while (state.keepRunning()) {
aliceCipher.multiRecipientEncrypt(recipients, content);
}
}
}
// Copied from SealedSessionCipherTest.java
private static SignedPreKeyRecord generateSignedPreKey(
IdentityKeyPair identityKeyPair, int signedPreKeyId) throws InvalidKeyException {
ECKeyPair keyPair = ECKeyPair.generate();
byte[] signature =
identityKeyPair.getPrivateKey().calculateSignature(keyPair.getPublicKey().serialize());
return new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
}
private static KyberPreKeyRecord generateKyberPreKey(
IdentityKeyPair identityKeyPair, int kyberPreKeyId) throws InvalidKeyException {
KEMKeyPair keyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024);
byte[] signature =
identityKeyPair.getPrivateKey().calculateSignature(keyPair.getPublicKey().serialize());
return new KyberPreKeyRecord(kyberPreKeyId, System.currentTimeMillis(), keyPair, signature);
}
private static SenderCertificate createCertificateFor(
ECKeyPair trustRoot,
UUID uuid,
String e164,
int deviceId,
ECPublicKey identityKey,
long expires)
throws InvalidKeyException, InvalidCertificateException {
ECKeyPair serverKey = ECKeyPair.generate();
ServerCertificate serverCertificate =
new ServerCertificate(trustRoot.getPrivateKey(), 1, serverKey.getPublicKey());
return serverCertificate.issue(
serverKey.getPrivateKey(),
uuid.toString(),
Optional.ofNullable(e164),
deviceId,
identityKey,
expires);
}
private static void initializeSessions(
InMemorySignalProtocolStore aliceStore,
InMemorySignalProtocolStore bobStore,
SignalProtocolAddress bobAddress,
SignalProtocolAddress aliceAddress)
throws InvalidKeyException, UntrustedIdentityException {
ECKeyPair bobPreKey = ECKeyPair.generate();
IdentityKeyPair bobIdentityKey = bobStore.getIdentityKeyPair();
SignedPreKeyRecord bobSignedPreKey = generateSignedPreKey(bobIdentityKey, 2);
KyberPreKeyRecord bobKyberPreKey = generateKyberPreKey(bobIdentityKey, 12);
PreKeyBundle bobBundle =
new PreKeyBundle(
1,
1,
1,
bobPreKey.getPublicKey(),
2,
bobSignedPreKey.getKeyPair().getPublicKey(),
bobSignedPreKey.getSignature(),
bobIdentityKey.getPublicKey(),
12,
bobKyberPreKey.getKeyPair().getPublicKey(),
bobKyberPreKey.getSignature());
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceStore, bobAddress, aliceAddress);
aliceSessionBuilder.process(bobBundle);
bobStore.storeSignedPreKey(2, bobSignedPreKey);
bobStore.storeKyberPreKey(12, bobKyberPreKey);
bobStore.storePreKey(1, new PreKeyRecord(1, bobPreKey));
}
}

View File

@ -1,15 +1,10 @@
import groovy.json.JsonSlurper
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'com.android.library' version '8.3.0'
id 'maven-publish'
id 'signing'
}
base {
archivesName = "libsignal-android"
}
archivesBaseName = "libsignal-android"
repositories {
google()
@ -18,23 +13,20 @@ repositories {
}
android {
namespace = 'org.signal.libsignal'
namespace 'org.signal.libsignal'
compileSdk 34
ndkVersion = '28.0.13004108'
ndkVersion '25.2.9519653'
defaultConfig {
minSdkVersion 23
minSdkVersion 21
targetSdkVersion 33
multiDexEnabled = true
testInstrumentationRunner "org.signal.libsignal.util.AndroidJUnitRunner"
// Automatically propagate matching environment variables into Java properties.
// See the custom AndroidJUnitRunner and TestEnvironment classes for more details.
testInstrumentationRunnerArguments["org.signal.libsignal.test.environment"] = collectTestEnvironment()
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
coreLibraryDesugaringEnabled = true
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
@ -46,10 +38,6 @@ android {
srcDir '../client/src/test/java'
srcDir '../shared/test/java'
}
kotlin {
srcDir '../client/src/test/java'
srcDir '../shared/test/java'
}
resources {
srcDir '../client/src/test/resources'
}
@ -57,8 +45,9 @@ android {
}
packagingOptions {
// Defer stripping to the Android app project.
doNotStrip '**/*.so'
jniLibs {
pickFirst 'lib/*/libsignal_jni.so'
}
}
publishing {
@ -66,71 +55,23 @@ android {
}
}
kotlin {
explicitApi()
}
task dokkaHtmlJar(type: Jar) {
dependsOn(dokkaGeneratePublicationHtml)
from(dokkaGeneratePublicationHtml)
archiveClassifier.set("dokka")
}
task dokkaJavadocJar(type: Jar) {
dependsOn(dokkaGeneratePublicationJavadoc)
from(dokkaGeneratePublicationJavadoc)
archiveClassifier.set("javadoc")
}
String collectTestEnvironment() {
def result = []
System.getenv().each { k, v ->
if (k.startsWith("LIBSIGNAL_TESTING_")) {
// Limit what characters we accept in values.
// This is going to get mashed down to a single command-line argument.
// (This pattern is only meant to head off likely problems and was not specifically
// tested; if you need to use one of these characters, you can remove the check and see
// if things Just Work, or tweak our AndroidJUnitRunner to handle different delimiters
// or escaping.)
if (v.matches(".*[, \t\r\n].*")) {
logger.warn("warning: ignoring ${k} for running tests; it contains invalid characters")
return
}
result << "${k}=${v}"
}
}
result.join(",")
}
// We include the classes and data for rustls-platform-verifier ourselves,
// but we want to make sure it's in sync with the Rust side.
// So we check that there hasn't been a new release of the Android package since we made our fork.
void checkRustlsPlatformVerifierVersion() {
def dependencyText = providers.exec {
it.workingDir = project.rootDir.parentFile
commandLine("bash", "java/find_cargo.sh", "metadata", "--format-version", "1")
}.standardOutput.asText.get()
def dependencyJson = new JsonSlurper().parseText(dependencyText)
def dependencyVersion = dependencyJson.packages.find { it.name == "rustls-platform-verifier-android" }.version
assert dependencyVersion == "0.1.1"
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
androidTestImplementation "androidx.test:runner:1.4.0"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'com.googlecode.json-simple:json-simple:1.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
androidTestImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
api project(':client')
}
tasks.register('libsWithDebugSymbols', Zip) {
from 'src/main/jniLibs'
archiveClassifier = 'debug-symbols'
dependsOn 'makeJniLibraries'
}
preBuild {
dependsOn 'collectAssets'
dependsOn 'makeJniLibraries'
dependsOn 'makeTestJniLibraries'
}
String[] archsFromProperty(String prop) {
@ -138,42 +79,43 @@ String[] archsFromProperty(String prop) {
}
task makeJniLibraries(type:Exec) {
group = 'Rust'
description = 'Build the JNI libraries for Android'
group 'Rust'
description 'Build the JNI libraries for Android'
def archs = archsFromProperty('androidArchs') ?: ['android']
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
def libsignalDebugFlag = project.hasProperty('libsignalDebug') ? ['--libsignal-debug'] : []
// Explicitly specify 'bash' for Windows compatibility.
commandLine 'bash', '../build_jni.sh', *libsignalDebugFlag, *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, *archs
commandLine 'bash', '../build_jni.sh', *archs
environment 'ANDROID_NDK_HOME', android.ndkDirectory
}
task makeTestJniLibraries(type:Exec) {
group 'Rust'
description 'Build JNI libraries for Android for testing'
def archs = archsFromProperty('androidTestingArchs') ?: archsFromProperty('androidArchs') ?: ['android']
// Explicitly specify 'bash' for Windows compatibility.
commandLine 'bash', '../build_jni.sh', '--testing', *archs
environment 'ANDROID_NDK_HOME', android.ndkDirectory
}
task collectAssets(type:Copy) {
from('../../acknowledgments') {
include 'acknowledgments-android*.md'
rename 'acknowledgments-android(.*)[.]md', 'libsignal$1.md'
from('../../acknowledgments/acknowledgments.md') {
rename 'acknowledgments.md', 'libsignal.md'
}
into 'src/main/assets/acknowledgments'
}
// MARK: Publication
afterEvaluate {
checkRustlsPlatformVerifierVersion()
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = base.archivesName.get()
artifactId = archivesBaseName
from components.release
artifact dokkaHtmlJar
artifact dokkaJavadocJar
artifact libsWithDebugSymbols
pom {
name = base.archivesName.get()
name = archivesBaseName
packaging = 'aar'
description = 'Signal Protocol cryptography library for Android'
url = 'https://github.com/signalapp/libsignal'
@ -203,7 +145,7 @@ afterEvaluate {
setUpSigningKey(signing)
signing {
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
required { isReleaseBuild() && gradle.taskGraph.hasTask(":android:publish") }
sign publishing.publications.mavenJava
}
}

View File

@ -1,40 +0,0 @@
plugins {
id 'com.android.library'
}
repositories {
google()
mavenCentral()
mavenLocal()
}
android {
compileSdk 34
defaultConfig {
minSdkVersion 23
targetSdkVersion 33
multiDexEnabled = true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
coreLibraryDesugaringEnabled = true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
packagingOptions {
jniLibs.excludes.add("**/libsignal_jni_testing.so")
}
namespace = "org.signal.libsignal.packagingtest"
}
dependencies {
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
androidTestImplementation project(':android')
}

View File

@ -1,36 +0,0 @@
//
// Copyright 2025 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal;
import static org.junit.Assert.assertThrows;
import org.junit.Test;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeTesting;
/**
* Tests that check that libsignal is loadable and available.
*
* <p>This test is expected to run in the production configuration, where only {@code
* libsignal_jni.so} is available (as opposed to the test/debug configuration, which also includes
* {@code libsignal_jni_testing.so}). The difference is important, since when both are available,
* {@code libsignal_jni_testing.so} is loaded, with <code>libsignal_jni.so</code> as a fallback.
* {@code libsignal_jni_testing.so} exposes a superset of the <code>
* libsignal_jni.so</code> API, including some test only functions, but the actual production
* configuration only loads {@code libsignal_jni.so}. These tests check that the custom loading code
* works correctly in production configurations in addition to the test/debug configuration.
*/
public class SmokeTest {
@Test
public void testCanCallNativeMethod() {
Native.keepAlive(null);
}
@Test
public void testCantCallNativeTestingMethod() {
assertThrows(UnsatisfiedLinkError.class, () -> NativeTesting.test_only_fn_returns_123());
}
}

View File

@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@ -1,36 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.util;
import android.os.Bundle;
import org.signal.libsignal.protocol.logging.AndroidSignalProtocolLogger;
import org.signal.libsignal.protocol.logging.SignalProtocolLogger;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
/** Custom setup for our JUnit tests, when run as instrumentation tests. */
public class AndroidJUnitRunner extends androidx.test.runner.AndroidJUnitRunner {
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
// Make sure libsignal logs get caught correctly.
SignalProtocolLoggerProvider.setProvider(
new TestLoggerDecorator(new AndroidSignalProtocolLogger()));
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.VERBOSE);
// Propagate any "environment variables" the test might need into System properties.
String testEnvironment = bundle.getString(TestEnvironment.PROPERTY_NAMESPACE);
if (testEnvironment != null) {
for (String joinedProp : testEnvironment.split(",")) {
String[] splitProp = joinedProp.split("=", 2);
if (splitProp.length != 2) {
continue;
}
System.setProperty(TestEnvironment.PROPERTY_NAMESPACE + "." + splitProp[0], splitProp[1]);
}
}
}
}

View File

@ -1,503 +0,0 @@
// Forked from https://github.com/rustls/rustls-platform-verifier/blob/v/0.5.1/android/rustls-platform-verifier/src/main/java/org/rustls/platformverifier/CertificateVerifier.kt.
// under the MIT License:
//
// Copyright (c) 2022 1Password
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// Additional modifications Copyright 2025 Signal Messenger, LLC.
// We use the same package and class name to avoid having to change the Rust side of the bridge.
package org.rustls.platformverifier
import android.annotation.SuppressLint
import android.content.Context
import android.net.http.X509TrustManagerExtensions
import android.os.Build
import android.util.Log
import java.io.ByteArrayInputStream
import java.io.File
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.MessageDigest
import java.security.PublicKey
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateFactory
import java.security.cert.CertificateNotYetValidException
import java.security.cert.CertificateParsingException
import java.security.cert.PKIXBuilderParameters
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import java.util.Date
import java.util.EnumSet
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import javax.security.auth.x500.X500Principal
// If this is updated, update the Rust definition too.
// Marked private as this is not meant to be used in Android code.
private enum class StatusCode(val value: Int) {
Ok(0),
Unavailable(1),
Expired(2),
UnknownCert(3),
Revoked(4),
InvalidEncoding(5),
InvalidExtension(6),
}
// Marked private as this is not meant to be used in Android code.
private class VerificationResult(
status: StatusCode,
@Suppress("unused") val message: String? = null
) {
@Suppress("unused")
private val code: Int = status.value
}
// ADDED BY SIGNAL: Takes the place of an Android library BuildConfig.
private object BuildConfig {
const val TEST: Boolean = false
}
// NOTE: All TrustManager and certificate validation methods are not thread safe. These
// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior.
// Only JNI and test code calls this, so unused code warnings are suppressed.
// Internal for test code - no other Kotlin code should use this object directly.
// MODIFIED FOR SIGNAL: exposed as public so we can set `shouldCheckRevocation`
@Suppress("unused")
// We want to show a difference between Kotlin-side logs and those in Rust code
@SuppressLint("LongLogTag")
public object CertificateVerifier {
private const val TAG = "rustls-platform-verifier-android"
// ADDED BY SIGNAL
@JvmStatic
public var shouldCheckRevocation: Boolean = false
private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? {
// This can never throw since the default algorithm is used.
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(keystore)
val availableTrustManagers = try {
factory.trustManagers
} catch (e: RuntimeException) {
Log.w(TAG, "exception thrown creating a TrustManager: $e")
return null
}
for (manager in availableTrustManagers) {
if (manager is X509TrustManager) {
// Kotlin ensures this can't throw at runtime since it knows that
// it must be the correct type by now.
return X509TrustManagerExtensions(manager)
}
}
Log.e(TAG, "failed to find a usable trust manager")
return null
}
private fun makeLazyTrustManager(keystore: KeyStore?): Lazy<X509TrustManagerExtensions?> {
// Ensure the keystore is loaded. Since all of the trust managers are initialized in a
// `Lazy`, this will only run once.
keystore?.load(null)
return lazy { createTrustManager(keystore) }
}
// -- Test only --
// Ideally, all of this will be optimized out at compile time due to not being accessed
// in release builds.
@get:Synchronized
private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
@get:Synchronized
private var mockTrustManager: Lazy<X509TrustManagerExtensions?> =
makeLazyTrustManager(mockKeystore)
@JvmStatic
private fun addMockRoot(root: ByteArray) {
if (!BuildConfig.TEST) {
throw Exception("attempted to add a mock root outside a test!")
}
val alias = "root_${mockKeystore.size()}"
// Throwing here is fine since test roots should always be well-formed
val cert = certFactory.generateCertificate(ByteArrayInputStream(root))
mockKeystore.setCertificateEntry(alias, cert)
reloadMockData()
}
@JvmStatic
private fun clearMockRoots() {
// Reload to get a completely fresh internal state
mockKeystore.load(null)
reloadMockData()
}
@JvmStatic
private fun reloadMockData() {
if (mockTrustManager.isInitialized()) {
mockTrustManager = makeLazyTrustManager(mockKeystore)
}
}
// Get a list of the system's root CAs.
// Function is public for testing only.
@JvmStatic
public fun getSystemRootCAs(): List<X509Certificate> {
val rootCAs = mutableListOf<X509Certificate>()
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
factory.init(systemKeystore)
val availableTrustManagers = try {
factory.trustManagers
} catch (e: RuntimeException) {
Log.w(TAG, "exception thrown creating a TrustManager: $e")
return rootCAs
}
availableTrustManagers.forEach { trustManager ->
if (trustManager is X509TrustManager) {
rootCAs.addAll(trustManager.acceptedIssuers)
}
}
return rootCAs
}
// -- End testing requirements --
private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509")
private var systemTrustAnchorCache = hashSetOf<Pair<X500Principal, PublicKey>>()
@get:Synchronized
private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath ->
File("$rootPath/etc/security/cacerts")
}
@get:Synchronized
private val systemKeystore: KeyStore? = try {
KeyStore.getInstance("AndroidCAStore")
} catch (_: KeyStoreException) {
null
}
@get:Synchronized
private val systemTrustManager: Lazy<X509TrustManagerExtensions?> =
makeLazyTrustManager(systemKeystore)
@JvmStatic
private fun verifyCertificateChain(
@Suppress("UNUSED_PARAMETER") context: Context,
serverName: String,
authMethod: String,
allowedEkus: Array<String>,
ocspResponse: ByteArray?,
time: Long,
certChain: Array<ByteArray>
): VerificationResult {
// Convert the array of (supposedly) DER bytes into certificates.
val certificateChain = mutableListOf<X509Certificate>()
certChain.forEach { certBytes ->
val certificate = try {
certFactory.generateCertificate(ByteArrayInputStream(certBytes))
} catch (e: CertificateException) {
return VerificationResult(StatusCode.InvalidEncoding)
}
certificateChain.add(certificate as X509Certificate)
}
// Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait
// has a mandatory `end_entity` parameter in `verify_server_cert`.
val endEntity = certificateChain[0]
// Check that the certificate is valid at the point of time provided by `rustls`.
try {
endEntity.checkValidity(Date(time))
} catch (e: CertificateExpiredException) {
return VerificationResult(StatusCode.Expired)
} catch (e: CertificateNotYetValidException) {
return VerificationResult(StatusCode.Expired)
}
// Check that this certificate can be used in a TLS server.
if (!verifyCertUsage(endEntity, allowedEkus)) {
return VerificationResult(StatusCode.InvalidExtension)
}
// Select the trust manager to use.
//
// We select them as follows:
// - If built for release, only use the system trust manager. This should let all test-related
// code be optimized out.
// - If built for tests:
// - If the mock CA store has any values, use the mock trust manager.
// - Otherwise, use the system trust manager.
val (trustManager, keystore) = if (!BuildConfig.TEST) {
val trustManager =
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
Pair(trustManager, systemKeystore)
} else {
if (mockKeystore.size() != 0) {
val trustManager = mockTrustManager.value!!
Pair(trustManager, mockKeystore)
} else {
val trustManager =
systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable)
Pair(trustManager, systemKeystore)
}
}
// Verify that the certificate chain is valid and correct, and nothing more.
//
// NOTE: This does not validate `serverName` is valid for the end-entity certificate.
// That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant
// hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24.
//
// `serverName` is only used for pinning/CT requirements.
//
// Returns the "the properly ordered chain used for verification as a list of X509Certificates.",
// meaning a list from end-entity certificate to trust-anchor.
val validChain = try {
trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName)
} catch (e: CertificateException) {
// In test configurations we may see `checkServerTrusted` fail once vendored test
// certificates pass their expiry date. We try to avoid that by using a fixed
// verification time when calling `endEntity.checkValidity` above, however we can't
// fix the time for the `checkServerTrusted` call.
//
// To make diagnosing CI test failures easier we try to find the root cause of
// checkServerTrusted failing, returning a different `StatusCode` as appropriate.
if (BuildConfig.TEST) {
var rootCause: Throwable? = e
while (rootCause?.cause != null && rootCause.cause != rootCause) {
rootCause = rootCause.cause
}
return when (rootCause) {
is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult(
StatusCode.Expired,
rootCause.toString()
)
else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString())
}
}
// In non-test configurations we should have caught expiry errors earlier and
// can simply return an unknown cert error without digging through the exception
// cause chain.
return VerificationResult(StatusCode.UnknownCert, e.toString())
}
// TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled,
// because Android requires certificates to an specify OCSP responder for network fetch in this case.
// If in testing w/o OCSP stapled, short-circuit here - only prior checks apply.
if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) {
return VerificationResult(StatusCode.Ok)
}
// Try to check the revocation status of the cert, if it is supported.
//
// This is supported at >= API 24, but we're supporting 22 (Android 5) for the best
// compatibility.
//
// MODIFIED BY SIGNAL: only if shouldCheckRevocation is set.
if (shouldCheckRevocation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Note:
//
// 1. Android does not provide any way only to attempt to validate revocation from cached
// data like the other platforms do. This means it will always use the network for
// certificates which had no stapled response.
//
// 2: Likely because of 1, Android requires all issued certificates to have some form of
// revocation included in their authority information. This doesn't work universally as
// issuing certificates in use may omit authority access information (for example the
// Let's Encrypt R3 Intermediate Certificate).
//
// Given these constraints, the best option is to only check revocation information
// at the end-entity depth. We will prefer OCSP (to use stapled information if possible).
// If there is no stapled OCSP response, Android may use the network to attempt to fetch
// one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft"
// failures, for example transient network errors.
//
// In the case of a non-public root, such as an internal CA or self-signed certificate,
// we opt to skip revocation checks entirely. The only exception is if the server
// provided stapled OCSP data, which is an explicit signal and won't introduce non-ideal
// platform behavior when attempting validation.
//
// This is because these are cases where a user or administrator has explicitly opted to
// trust a certificate they (at least believe) have control over. These certificates rarely
// contain revocation information as well, so these cases don't lose much.
// See https://github.com/rustls/rustls-platform-verifier/issues/69 as well.
if (ocspResponse == null && !isKnownRoot(validChain.last())) {
// Chain validation must have succeeded by this point.
return VerificationResult(StatusCode.Ok)
}
val parameters = PKIXBuilderParameters(keystore, null)
val validator = CertPathValidator.getInstance("PKIX")
val revocationChecker = validator.revocationChecker as PKIXRevocationChecker
revocationChecker.options = EnumSet.of(
PKIXRevocationChecker.Option.SOFT_FAIL,
PKIXRevocationChecker.Option.ONLY_END_ENTITY
)
// Use the OCSP data `rustls` provided, if present.
// Its expected that the server only sends revocation data for its own leaf certificate.
//
// If this field is set, then Android will use it and skip any networking to
// attempt a fetch for that certificate. Otherwise, it will attempt to fetch it from the network.
// Ref: https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/provider/certpath/RevocationChecker.java;l=694
ocspResponse?.let { providedResponse ->
revocationChecker.ocspResponses = mapOf(endEntity to providedResponse)
}
// Use the custom revocation definition.
// "Note that when a `PKIXRevocationChecker` is added to `PKIXParameters`, it clones the `PKIXRevocationChecker`;
// thus any subsequent modifications to the `PKIXRevocationChecker` have no effect."
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
parameters.certPathCheckers = listOf(revocationChecker)
// "When supplying a revocation checker in this manner, it will be used to check revocation
// irrespective of the setting of the `RevocationEnabled` flag."
// - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker
parameters.isRevocationEnabled = false
// Validate the revocation status of the end entity certificate.
try {
validator.validate(certFactory.generateCertPath(validChain), parameters)
} catch (e: CertPathValidatorException) {
return VerificationResult(StatusCode.Revoked, e.toString())
}
// MODIFIED BY SIGNAL: The warning log used to be unconditional.
} else if (shouldCheckRevocation) {
// This is allowed to be skipped since revocation checking is best-effort.
Log.w(TAG, "did not attempt to validate OCSP due to Android version")
} else {
Log.v(TAG, "note: revocation checking disabled")
}
return VerificationResult(StatusCode.Ok)
}
private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array<String>): Boolean {
val ekus = try {
certificate.extendedKeyUsage
}
// This should be unreachable, but could happen.
catch (_: CertificateParsingException) {
return false
} catch (_: NullPointerException) {
// According to Chromium's implementation, this can crash when the EKU data is malformed.
Log.w(TAG, "exception handling certificate EKU")
return false
} ?: return true // If the list is empty, we have nothing to do.
return ekus.any { allowedEkus.contains(it) }
}
// Android hashes a principal using the first four bytes of its MD5 digest, encoded in
// lowercase hex and reversed.
//
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339
private fun hashPrincipal(principal: X500Principal): String {
val hexDigits = "0123456789abcdef".toCharArray()
val digest = MessageDigest.getInstance("MD5").digest(principal.encoded)
val hexChars = CharArray(8)
for (i in 0..3) {
// Kotlin doesn't support bitwise operators for bytes, only Int and Long.
val digestByte = digest[3 - i].toInt()
hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf]
hexChars[2 * i + 1] = hexDigits[digestByte and 0xf]
}
return String(hexChars)
}
// Check if CA root is known or not.
// Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user.
//
// Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351
public fun isKnownRoot(root: X509Certificate): Boolean {
// System keystore and cert directory must be non-null to perform checking
systemKeystore?.let { loadedSystemKeystore ->
systemCertificateDirectory?.let { loadedSystemCertificateDirectory ->
// Check the in-memory cache first
val key = Pair(root.subjectX500Principal, root.publicKey)
if (systemTrustAnchorCache.contains(key)) {
return true
}
// System trust anchors are stored under a hash of the principal.
// In case of collisions, append number.
val hash = hashPrincipal(root.subjectX500Principal)
var i = 0
while (true) {
val alias = "$hash.$i"
if (!File(loadedSystemCertificateDirectory, alias).exists()) {
break
}
val anchor = loadedSystemKeystore.getCertificate("system:$alias")
// It's possible for `anchor` to be `null` if the user deleted a trust anchor.
// Continue iterating as there may be further collisions after the deleted anchor.
if (anchor == null) {
continue
// This should never happen
} else if (anchor !is X509Certificate) {
// SAFETY: This logs a unique identifier (hash value) only in cases where a file within the
// system's root trust store is not a valid X509 certificate (extremely unlikely error).
// The hash doesn't tell us any sensitive information about the invalid cert or reveal any of
// its contents - it just lets us ID the bad file if a user is having TLS failure issues.
Log.e(TAG, "anchor is not a certificate, alias: $alias")
continue
// If subject and public key match, it's a system root.
} else {
if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) {
systemTrustAnchorCache.add(key)
return true
}
}
i += 1
}
}
}
// Not found in cache or store: non-public
return false
}
}

View File

@ -1,23 +0,0 @@
plugins {
id 'application'
}
repositories {
google()
mavenCentral()
mavenLocal()
}
application {
mainClass = "BackupTool"
}
dependencies {
implementation project(':client')
implementation 'info.picocli:picocli:4.7.6'
annotationProcessor 'info.picocli:picocli-codegen:4.7.6'
}
compileJava {
options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] // recommended by picocli
}

View File

@ -1,61 +0,0 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.concurrent.Callable;
import org.signal.libsignal.messagebackup.MessageBackup;
import org.signal.libsignal.messagebackup.MessageBackupKey;
import org.signal.libsignal.protocol.logging.SignalProtocolLogger;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.libsignal.protocol.util.Hex;
import picocli.CommandLine;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
class BackupTool implements Callable<Integer> {
@Option(names = "--hmac-key")
String hmacKey;
@Option(names = "--aes-key")
String aesKey;
@Parameters File input;
public static void main(String[] args) {
int exitCode = new CommandLine(new BackupTool()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() throws Exception {
SignalProtocolLoggerProvider.initializeLogging(SignalProtocolLogger.INFO);
SignalProtocolLoggerProvider.setProvider(
new SignalProtocolLogger() {
public void log(int priority, String tag, String message) {
System.err.println(priority + " " + message);
}
});
byte[] hmacKey = Hex.fromStringCondensed(this.hmacKey);
byte[] aesKey = Hex.fromStringCondensed(this.aesKey);
var backupKey = MessageBackupKey.fromParts(hmacKey, aesKey);
MessageBackup.ValidationResult result =
MessageBackup.validate(
backupKey,
MessageBackup.Purpose.REMOTE_BACKUP,
() -> {
try {
return new FileInputStream(input);
} catch (FileNotFoundException e) {
throw new AssertionError(e);
}
},
input.length());
return result.unknownFieldMessages.length == 0 ? 0 : 1;
}
}

View File

@ -1,92 +1,66 @@
import org.gradle.api.publish.PublishingExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id "base"
id "signing"
id "com.diffplug.spotless" version "7.2.1"
id "org.jetbrains.kotlin.jvm" version "2.2.20" apply false
id "org.jetbrains.dokka" version "2.1.0" apply false
id "org.jetbrains.dokka-javadoc" version "2.1.0" apply false
// These plugins need to be loaded together, so we must declare them up front.
id 'com.android.library' version "8.13.2" apply false
id 'org.jetbrains.kotlin.android' version "2.2.20" apply false
}
repositories {
mavenCentral()
google()
mavenLocal()
id "com.diffplug.spotless" version "6.20.0"
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
}
allprojects {
version = "0.94.1"
version = "0.41.2"
group = "org.signal"
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs += ["-Xlint:deprecation", "-Xlint:fallthrough", "-Xlint:unchecked"]
}
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
options.addStringOption('Xdoclint:none', '-quiet')
}
tasks.withType(KotlinCompile).configureEach {
compilerOptions.jvmTarget = JvmTarget.JVM_17
}
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
subprojects {
if (JavaVersion.current().isJava8Compatible()) {
allprojects {
tasks.withType(Javadoc) {
options.encoding = 'UTF-8'
options.addStringOption('Xdoclint:none', '-quiet')
}
}
}
apply plugin: "org.jetbrains.dokka"
apply plugin: "org.jetbrains.dokka-javadoc"
allprojects {
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
options.compilerArgs += ["-Xlint:deprecation", "-Xlint:fallthrough", "-Xlint:unchecked"]
}
}
apply plugin: "com.diffplug.spotless"
spotless {
java {
target('**/*.java')
targetExclude('**/Native.java')
importOrder()
removeUnusedImports()
googleJavaFormat()
formatAnnotations()
licenseHeaderFile rootProject.file('license_header.txt')
}
}
}
task makeJniLibrariesDesktop(type:Exec) {
group = 'Rust'
description = 'Build the JNI libraries'
group 'Rust'
description 'Build the JNI libraries'
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
// Explicitly specify 'bash' for Windows compatibility.
commandLine 'bash', './build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, 'desktop'
}
task makeJniLibrariesServer(type:Exec) {
group = 'Rust'
description = 'Build the JNI libraries'
def debugLevelLogsFlag = project.hasProperty('debugLevelLogs') ? ['--debug-level-logs'] : []
def jniTypeTaggingFlag = project.hasProperty('jniTypeTagging') ? ['--jni-type-tagging'] : []
def jniCheckAnnotationsFlag = project.hasProperty('jniCheckAnnotations') ? ['--jni-check-annotations'] : []
def debugFlag = project.hasProperty('debugRust') ? ['--debug'] : []
def target = project.hasProperty('crossCompileServer') ? 'server-all' : 'server'
// Explicitly specify 'bash' for Windows compatibility.
commandLine 'bash', './build_jni.sh', *debugLevelLogsFlag, *jniTypeTaggingFlag, *jniCheckAnnotationsFlag, *debugFlag, target
commandLine 'bash', './build_jni.sh', 'desktop'
}
task cargoClean(type:Exec) {
group = 'Rust'
group 'Rust'
commandLine 'cargo', 'clean'
}
task cleanJni(type: Delete) {
description = 'Clean JNI libs'
description 'Clean JNI libs'
delete fileTree('./android/src/main/jniLibs') {
include '**/*.so'
}
delete fileTree('./client/src/main/resources') {
include '**/*.so'
include '**/*.dylib'
include '**/*.dll'
}
delete fileTree('./server/src/main/resources') {
delete fileTree('./shared/resources') {
include '**/*.so'
include '**/*.dylib'
include '**/*.dll'
@ -97,7 +71,7 @@ clean.dependsOn([cargoClean, cleanJni])
// PUBLISHING
ext.setUpSigningKey = { signingExt ->
ext.setUpSigningKey = { signingExt ->
def signingKeyId = findProperty("signingKeyId")
def signingKey = findProperty("signingKey")
def signingPassword = findProperty("signingPassword")
@ -106,25 +80,11 @@ ext.setUpSigningKey = { signingExt ->
}
}
subprojects { subproject ->
subproject.plugins.withId('maven-publish') {
subproject.extensions.configure(PublishingExtension) { publishing ->
publishing.repositories {
maven {
name = "SignalBuildArtifacts"
// We can't use Gradle's built-in GCS support with the way we authenticate
// GitHub Actions. Fortunately, GCS's REST APIs are basically just normal HTTP
// GET/PUT with an auth token, which is compatible with what Gradle will do.
url = subproject.uri("https://storage.googleapis.com/build-artifacts.signal.org/libraries/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
value = "Bearer ${System.getenv("CLOUDSDK_AUTH_ACCESS_TOKEN") ?: ""}"
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
nexusPublishing {
repositories {
sonatype {
username = project.findProperty('sonatypeUsername') ?: ""
password = project.findProperty('sonatypePassword') ?: ""
}
}
}
@ -132,32 +92,3 @@ subprojects { subproject ->
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
// Late evaluation after this point.
evaluationDependsOnChildren()
spotless {
kotlin {
target allprojects.collectMany {
return it.tasks.withType(KotlinCompile)
}.inject(files()) { collected, next ->
collected + next.sources
}
targetExclude('**/Native.kt', '**/NativeTesting.kt', '**/org/rustls/**')
ktlint()
}
java {
target allprojects.collectMany {
return it.tasks.withType(JavaCompile)
}.inject(files()) { collected, next ->
collected + next.source
}
importOrder()
removeUnusedImports()
googleJavaFormat()
formatAnnotations()
licenseHeaderFile rootProject.file('license_header.txt')
}
}

View File

@ -13,162 +13,41 @@ cd "${SCRIPT_DIR}"/..
# These paths are relative to the root directory
ANDROID_LIB_DIR=java/android/src/main/jniLibs
DESKTOP_LIB_DIR=java/client/src/main/resources
SERVER_LIB_DIR=java/server/src/main/resources
DESKTOP_LIB_DIR=java/shared/resources
# Fetch dependencies first, so we can use them in computing later options.
# But allow this to fail in case we're offline.
cargo fetch || true
export CARGO_PROFILE_RELEASE_DEBUG=1 # enable line tables
export CARGO_PROFILE_RELEASE_OPT_LEVEL=s # optimize for size over speed
export CARGO_PROFILE_RELEASE_DEBUG=1 # Enable line tables
RUSTFLAGS="--cfg aes_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
RUSTFLAGS="--cfg tokio_unstable ${RUSTFLAGS:-}" # Access tokio's unstable metrics
RUSTFLAGS="$(rust_remap_path_options) ${RUSTFLAGS:-}" # Strip absolute paths
export RUSTFLAGS
DEBUG_LEVEL_LOGS=
JNI_TYPE_TAGGING=
RUST_RELEASE="release"
while [ "${1:-}" != "" ]; do
case "${1:-}" in
--debug-level-logs )
DEBUG_LEVEL_LOGS=1
shift
;;
--libsignal-debug )
LIBSIGNAL_DEBUG=1
shift
;;
--jni-type-tagging )
JNI_TYPE_TAGGING=1
shift
;;
--jni-check-annotations )
JNI_CHECK_ANNOTATIONS=1
shift
;;
--debug )
RUST_RELEASE=
shift
;;
-* )
echo "Unrecognized flag $1; expected --debug-level-logs, --jni-type-tagging, or --debug" >&2
exit 2
;;
*)
break
esac
done
if [[ -z "${DEBUG_LEVEL_LOGS:-}" ]]; then
FEATURES+=("log/release_max_level_info")
fi
if [[ -n "${JNI_TYPE_TAGGING:-}" ]]; then
FEATURES+=("libsignal-bridge-types/jni-type-tagging")
fi
if [[ -n "${JNI_CHECK_ANNOTATIONS:-}" ]]; then
FEATURES+=("libsignal-bridge-types/jni-invoke-annotated")
fi
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
FEATURES+=("libsignal-debug/enabled")
fi
# usage: check_for_debug_level_logs_if_needed lib_dir
check_for_debug_level_logs_if_needed () {
if [[ "${RUST_RELEASE}" == "" ]]; then
# Unused strings are only stripped in release builds, not debug builds,
# so the check below won't tell us anything.
return
fi
if [[ -z "${DEBUG_LEVEL_LOGS:-}" ]]; then
# See libsignal-jni's logging.rs for the strings matched by this pattern.
# Searching *every* file in the lib directory is probably overkill,
# but it's easier than figuring out prefixes and suffixes like copy_built_library does.
if grep -q -- '-LEVEL LOGS ENABLED' "$1"/*; then
echo 'error: debug-level logs found in build that should not have them!' >&2
exit 2
fi
fi
# See libsignal-debug for the strings matched by this pattern.
if grep -q -- 'LIBSIGNAL-DEBUG IS ENABLED' "$1"/*; then
if [[ -z "${LIBSIGNAL_DEBUG:-}" ]]; then
echo 'error: libsignal-debug found in build that should not have it!' >&2
exit 2
fi
else
if [[ -n "${LIBSIGNAL_DEBUG:-}" ]]; then
echo 'error: libsignal-debug NOT found in build that SHOULD have it!' >&2
exit 2
fi
fi
}
# usage: build_desktop_for_arch target_triple host_triple output_dir
build_desktop_for_arch () {
local CC
local CXX
local CPATH
local lib_dir="${3}/"
local cpuarch="${1%%-*}"
case "$cpuarch" in
x86_64)
suffix=amd64
;;
aarch64)
suffix=aarch64
;;
*)
echo "building for unknown CPU architecture ${cpuarch}; update build_jni.sh"
exit 2
esac
if [[ "$1" != "$2" ]]; then
# Set up cross-compiling flags
if [[ "$1" == *-linux-* && "$2" == *-linux-* && -z "${CC:-}" ]]; then
# When cross-compiling *from* Linux *to* Linux,
# set up standard cross-compiling environment if not already set
echo 'setting Linux cross-compilation options...'
export "CARGO_TARGET_$(echo "$cpuarch" | tr "[:lower:]" "[:upper:]")_UNKNOWN_LINUX_GNU_LINKER"="${cpuarch}-linux-gnu-gcc"
export CC="${cpuarch}-linux-gnu-gcc"
export CXX="${cpuarch}-linux-gnu-g++"
export CPATH="/usr/${cpuarch}-linux-gnu/include"
fi
fi
echo_then_run cargo build -p libsignal-jni -p libsignal-jni-testing ${RUST_RELEASE:+--release} ${FEATURES:+--features "${FEATURES[*]}"} --target "$1"
copy_built_library "target/${1}/${RUST_RELEASE:-debug}" signal_jni "$lib_dir" "signal_jni_${suffix}"
copy_built_library "target/${1}/${RUST_RELEASE:-debug}" signal_jni_testing "$lib_dir" "signal_jni_testing_${suffix}"
check_for_debug_level_logs_if_needed "$lib_dir"
}
BUILD_FOR_TEST=
case "${1:-}" in
--testing )
BUILD_FOR_TEST=1
shift
;;
-* )
echo "Unrecognized flag $1; use --testing to compile with test functions" >&2
exit 2
;;
*)
# Do nothing
;;
esac
android_abis=()
while [ "${1:-}" != "" ]; do
case "${1:-}" in
desktop | server | server-all )
if [[ "$1" == desktop ]]; then
lib_dir=$DESKTOP_LIB_DIR
else
lib_dir=$SERVER_LIB_DIR
fi
desktop )
# On Linux, cdylibs don't include public symbols from their dependencies,
# even if those symbols have been re-exported in the Rust source.
# Using LTO works around this at the cost of a slightly slower build.
# https://github.com/rust-lang/rfcs/issues/2771
export CARGO_PROFILE_RELEASE_LTO=thin
host_triple=$(rustc -vV | sed -n 's|host: ||p')
if [[ "$1" == "server-all" ]]; then
build_desktop_for_arch x86_64-unknown-linux-gnu "$host_triple" $lib_dir
# Enable ARMv8.2 extensions for a production aarch64 server build
RUSTFLAGS="-C target-feature=+v8.2a ${RUSTFLAGS:-}" \
build_desktop_for_arch aarch64-unknown-linux-gnu "$host_triple" $lib_dir
else
build_desktop_for_arch "${CARGO_BUILD_TARGET:-$host_triple}" "$host_triple" $lib_dir
echo_then_run cargo build -p libsignal-jni --release --features testing-fns
if [[ -z "${CARGO_BUILD_TARGET:-}" ]]; then
copy_built_library target/release signal_jni "${DESKTOP_LIB_DIR}/"
fi
exit
;;
android )
android_abis+=(arm64-v8a armeabi-v7a x86_64 x86)
;;
@ -192,38 +71,19 @@ while [ "${1:-}" != "" ]; do
shift
done
if (( ${#android_abis[@]} == 0 )); then
echo "Missing target (use 'desktop', 'android', or 'android-\$ARCH')" >&2
exit 2
fi
# Everything from here down is Android-only.
export CARGO_PROFILE_RELEASE_OPT_LEVEL=s # optimize for size over speed
# Use full LTO and small BoringSSL curve tables to reduce binary size.
export CFLAGS="-DOPENSSL_SMALL -flto=full ${CFLAGS:-}"
export CXXFLAGS="-DOPENSSL_SMALL -flto=full ${CXXFLAGS:-}"
export CARGO_PROFILE_RELEASE_LTO=fat
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1
# Instruct boring-sys to autolink the *static* libc++, and to delay that linking until the final
# build product (the -bundle part). This is consistent with Google's advice for Android JNI
# libraries that are not part of the main app build[1], elaborated on in a GitHub thread[2]. It's
# also what we're doing with WebRTC. The syntax comes from rustc[3] via Cargo's rustc-link-lib build
# script feature.
#
# [1]: https://developer.android.com/ndk/guides/middleware-vendors#using_the_stl
# [2]: https://github.com/android/ndk/issues/796
# [3]: https://doc.rust-lang.org/rustc/command-line-arguments.html#-l-link-the-generated-crate-to-a-native-library
export BORING_BSSL_RUST_CPPLIB="static:-bundle=c++"
# Use the Android NDK's prebuilt Clang+lld as pqcrypto's compiler and Rust's linker.
ANDROID_TOOLCHAIN_DIR=$(echo "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt"/*/bin/)
ANDROID_MIN_SDK_VERSION=23
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi${ANDROID_MIN_SDK_VERSION}-clang"
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android${ANDROID_MIN_SDK_VERSION}-clang"
export CC_aarch64_linux_android="${ANDROID_TOOLCHAIN_DIR}/aarch64-linux-android21-clang"
export CC_armv7_linux_androideabi="${ANDROID_TOOLCHAIN_DIR}/armv7a-linux-androideabi21-clang"
export CC_x86_64_linux_android="${ANDROID_TOOLCHAIN_DIR}/x86_64-linux-android21-clang"
export CC_i686_linux_android="${ANDROID_TOOLCHAIN_DIR}/i686-linux-android21-clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${CC_aarch64_linux_android}"
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${CC_armv7_linux_androideabi}"
@ -231,11 +91,17 @@ export CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="${CC_x86_64_linux_android}"
export CARGO_TARGET_I686_LINUX_ANDROID_LINKER="${CC_i686_linux_android}"
export TARGET_AR="${ANDROID_TOOLCHAIN_DIR}/llvm-ar"
export RUSTFLAGS="--cfg aes_armv8 --cfg polyval_armv8 ${RUSTFLAGS:-}" # Enable ARMv8 cryptography acceleration when available
# The 64-bit curve25519-dalek backend is faster than the 32-bit one on at least some armv7a phones.
# Comment out the following to allow the 32-bit backend on 32-bit targets.
export RUSTFLAGS="--cfg curve25519_dalek_bits=\"64\" ${RUSTFLAGS:-}"
if [ $BUILD_FOR_TEST ]; then
FEATURES="testing-fns"
ANDROID_LIB_DIR="${ANDROID_LIB_DIR}/../../androidTest/jniLibs"
fi
target_for_abi() {
case "$1" in
arm64-v8a)
@ -257,9 +123,7 @@ target_for_abi() {
esac
}
for abi in "${android_abis[@]}"; do
rust_target=$(target_for_abi "$abi")
echo_then_run cargo build -p libsignal-jni -p libsignal-jni-testing ${RUST_RELEASE:+--release} ${FEATURES:+--features "${FEATURES[*]}"} -Z unstable-options --target "$rust_target" --artifact-dir "${ANDROID_LIB_DIR}/$abi" --timings
check_for_debug_level_logs_if_needed "${ANDROID_LIB_DIR}/$abi"
echo_then_run cargo build -p libsignal-jni --release ${FEATURES:+--features $FEATURES} -Z unstable-options --target "$rust_target" --out-dir "${ANDROID_LIB_DIR}/$abi"
done

View File

@ -5,130 +5,95 @@
# SPDX-License-Identifier: AGPL-3.0-only
#
import glob
import json
import os
import json
import subprocess
import sys
from typing import Any, Callable, Iterable, List, Mapping, Optional, TypeVar
T = TypeVar('T')
def warn(message: str) -> None:
def warn(message):
if 'GITHUB_ACTIONS' in os.environ:
print('::warning ::' + message)
print("::warning ::" + message)
else:
print('warning: ' + message, file=sys.stderr)
print("warning: " + message, file=sys.stderr)
def measure_stripped_library_size(lib_path: str) -> int:
ndk_home = os.environ.get('ANDROID_NDK_HOME')
if not ndk_home:
raise Exception('must set ANDROID_NDK_HOME to an Android NDK to run this script')
strip_glob = os.path.join(ndk_home, 'toolchains', 'llvm', 'prebuilt', '*', 'bin', 'llvm-strip')
strip = next(glob.iglob(strip_glob), None)
if not strip:
raise Exception('NDK does not contain llvm-strip (tried {})'.format(strip_glob))
return len(subprocess.check_output([strip, '-o', '-', lib_path]))
def print_size_diff(lib_size: int, old_entry: Mapping[str, Any], *, warn_on_jump: bool = True) -> None:
def print_size_diff(lib_size, old_entry):
delta = lib_size - old_entry['size']
delta_fraction = (float(delta) / old_entry['size'])
message = f"current build is {delta} bytes ({int(delta_fraction * 100)}%) larger than {old_entry['version']}"
if warn_on_jump and delta > 100_000:
message = "current build is {0}% larger than {1} (current: {2} bytes, {1}: {3} bytes)".format(
int(delta_fraction * 100),
old_entry['version'],
lib_size,
old_entry['size']
)
if delta_fraction > 0.10:
warn(message)
else:
print(message)
def print_size_for_release(lib_size: int) -> None:
message = f'if this this commit marks a release, update code_size.json with {lib_size}'
print(message)
def current_origin_main_entry() -> Optional[Mapping[str, Any]]:
def current_origin_main_entry():
try:
if os.environ.get('GITHUB_EVENT_NAME') == 'push':
base_ref = os.environ.get('GITHUB_REF_NAME', 'HEAD^')
most_recent_commit = subprocess.run(['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True).stdout.decode().strip()
else:
base_ref = os.environ.get('GITHUB_BASE_REF', 'main')
remote_name = os.environ.get('CHECK_CODE_SIZE_REMOTE', 'origin')
most_recent_commit = subprocess.run(['git', 'merge-base', 'HEAD', f'{remote_name}/{base_ref}'], capture_output=True, check=True).stdout.decode().strip()
most_recent_main = subprocess.run(["git", "merge-base", "HEAD", "origin/main"], capture_output=True, check=True).stdout.decode().strip()
repo_path = os.environ.get('GITHUB_REPOSITORY')
if repo_path is None:
repo_path = subprocess.run(['gh', 'repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'], capture_output=True, check=True).stdout.decode().strip()
repo_path = subprocess.run(["gh", "repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], capture_output=True, check=True).stdout.decode().strip()
runs_info = subprocess.run(['gh', 'api', '--method=GET', f'repos/{repo_path}/actions/runs', '-f', f'head_sha={most_recent_commit}'], capture_output=True, check=True).stdout
runs_info = subprocess.run(["gh", "api", "--method=GET", f"repos/{repo_path}/actions/runs", "-f", f"head_sha={most_recent_main}"], capture_output=True, check=True).stdout
runs_json = json.loads(runs_info)
run_id = [run['id'] for run in runs_json['workflow_runs'] if 'Build and Test' in run['name']][0]
run_id = [run['id'] for run in runs_json['workflow_runs'] if run['name'] == 'Build and Test'][0]
run_jobs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, f'{run_id}', '--json', 'jobs'], capture_output=True, check=True).stdout
run_jobs = subprocess.run(["gh", "run", "view", f"{run_id}", "--json", "jobs"], capture_output=True, check=True).stdout
jobs_json = json.loads(run_jobs)
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == 'Java Android'][0]
job_id = [job['databaseId'] for job in jobs_json['jobs'] if job['name'] == "Java"][0]
job_logs = subprocess.run(['gh', 'run', 'view', '-R', repo_path, '--job', f'{job_id}', '--log'], capture_output=True, check=True).stdout.decode()
job_logs = subprocess.run(["gh", "run", "view", "--job", f"{job_id}", "--log"], capture_output=True, check=True).stdout.decode()
for line in job_logs.splitlines():
if 'update code_size.json with' in line:
(_, bytes_count) = line.rsplit(' ', maxsplit=1)
return {'size': int(bytes_count), 'version': f'{most_recent_commit[:6]} ({base_ref})'}
if "check_code_size.py" in line and "current build" in line:
(_, after) = line.split("(current: ", maxsplit=1)
(bytes_count, _) = after.split(" ", maxsplit=1)
return {'size': int(bytes_count), 'version': most_recent_main[:6] + ' (main)'}
print(f'skipping checking current {base_ref} (most recent run did not include check_code_size.py)', file=sys.stderr)
except Exception as e:
print(f'skipping checking current {base_ref}: {e}', file=sys.stderr)
if isinstance(e, subprocess.CalledProcessError):
print('stdout:', e.stdout.decode(), file=sys.stderr)
print('stderr:', e.stderr.decode(), file=sys.stderr)
return None
return None
except subprocess.CalledProcessError as e:
print("not checking current origin/main:", e, file=sys.stderr)
print("stdout:", e.stdout.decode(), file=sys.stderr)
print("stderr:", e.stderr.decode(), file=sys.stderr)
our_abs_dir = os.path.dirname(os.path.realpath(__file__))
lib_size = measure_stripped_library_size(os.path.join(
our_abs_dir, 'android', 'src', 'main', 'jniLibs', 'arm64-v8a', 'libsignal_jni.so'))
lib_size = os.path.getsize(os.path.join(
our_abs_dir, 'android', 'build', 'intermediates', 'stripped_native_libs', 'release', 'stripReleaseDebugSymbols',
'out', 'lib', 'arm64-v8a', 'libsignal_jni.so'))
with open(os.path.join(our_abs_dir, 'code_size.json')) as old_sizes_file:
old_sizes = json.load(old_sizes_file)
most_recent_tag_entry = old_sizes[-1]
print_size_diff(lib_size, most_recent_tag_entry)
origin_main_entry = current_origin_main_entry()
if origin_main_entry is not None:
print_size_diff(lib_size, most_recent_tag_entry, warn_on_jump=False)
print_size_diff(lib_size, origin_main_entry)
else:
print_size_diff(lib_size, most_recent_tag_entry)
print_size_for_release(lib_size)
# Typing this properly requires a bunch of helpers in Python 3.9,
# and we don't have a strict type at the use site anyway.
def max_map(items: Iterable[T], transform: Callable[[T], Any]) -> Any:
return transform(max(items, key=transform))
def print_plot(sizes):
highest_size = max(recent_sizes, key=lambda x: x['size'])['size']
def print_plot(sizes: List[Mapping[str, Any]]) -> None:
highest_size = max_map(recent_sizes, lambda x: x['size'])
version_width = max_map(recent_sizes, lambda x: len(x['version']))
scale = 1.0 * 1024 * 1024
scale = 1 * 1024 * 1024
while scale < highest_size:
scale *= 2
scale /= 20
plot_width = int(highest_size / scale) + 1
for entry in sizes:
bucket = int(entry['size'] / scale) + 1
print('{:>{}}: {:<{}} ({} bytes)'.format(entry['version'], version_width, '*' * bucket, plot_width, entry['size']))
print('{:>14}: {} ({} bytes)'.format(entry['version'], '*' * bucket, entry['size']))
recent_sizes = old_sizes[-10:]

View File

@ -1,22 +1,11 @@
buildscript {
dependencies {
// This isn't compatible with the `plugins` lookup method, so it has to
// be declared in a `buildscript` block. See
// https://github.com/gradle/gradle/issues/1541 for info.
classpath 'com.guardsquare:proguard-gradle:7.4.2'
}
}
plugins {
id 'java-library'
id 'maven-publish'
id 'signing'
id 'org.jetbrains.kotlin.jvm'
}
base {
archivesName = "libsignal-client"
}
sourceCompatibility = 17
archivesBaseName = "libsignal-client"
repositories {
mavenCentral()
@ -29,11 +18,6 @@ sourceSets {
// Include libsignal sources shared between the client and server
srcDir '../shared/java'
}
kotlin {
srcDir 'src/main/java'
// Include libsignal sources shared between the client and server
srcDir '../shared/java'
}
resources {
srcDir '../shared/resources'
}
@ -42,60 +26,28 @@ sourceSets {
java {
srcDir '../shared/test/java'
}
kotlin {
srcDir 'src/test/java'
srcDir '../shared/test/java'
}
}
}
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'com.googlecode.json-simple:json-simple:1.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test:2.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2'
}
test {
jvmArgs '-Xcheck:jni'
testLogging {
events 'passed','skipped','failed'
events 'passed'
showStandardStreams = true
showExceptions = true
exceptionFormat = 'full'
showCauses = true
showStackTraces = true
showExceptions true
exceptionFormat 'full'
showCauses true
showStackTraces true
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
withSourcesJar()
}
kotlin {
explicitApi()
}
sourcesJar {
// Cut down on artifact size by leaving these out of the sources jar.
exclude '*.dll'
exclude '*.dylib'
exclude '*.so'
}
task dokkaHtmlJar(type: Jar) {
dependsOn(dokkaGeneratePublicationHtml)
from(dokkaGeneratePublicationHtml)
archiveClassifier.set("dokka")
}
task dokkaJavadocJar(type: Jar) {
dependsOn(dokkaGeneratePublicationJavadoc)
from(dokkaGeneratePublicationJavadoc)
archiveClassifier.set("javadoc")
withJavadocJar()
}
tasks.named('jar') {
@ -105,6 +57,7 @@ tasks.named('jar') {
}
processResources {
// TODO: Build a different variant of the JNI library for server.
dependsOn ':makeJniLibrariesDesktop'
}
@ -113,13 +66,11 @@ processResources {
publishing {
publications {
mavenJava(MavenPublication) {
artifactId = base.archivesName.get()
artifactId = archivesBaseName
from components.java
artifact dokkaHtmlJar
artifact dokkaJavadocJar
pom {
name = base.archivesName.get()
name = archivesBaseName
description = 'Signal Protocol cryptography library for Java'
url = 'https://github.com/signalapp/libsignal'
@ -148,6 +99,6 @@ publishing {
setUpSigningKey(signing)
signing {
required = { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
required { isReleaseBuild() && gradle.taskGraph.hasTask(":client:publish") }
sign publishing.publications.mavenJava
}

View File

@ -9,25 +9,23 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import java.time.Instant;
import org.signal.libsignal.attest.AttestationDataException;
import org.signal.libsignal.attest.AttestationFailedException;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.sgxsession.SgxClient;
/**
* Cds2Client provides bindings to interact with Signal's v2 Contact Discovery Service.
*
* <p>See the {@link SgxClient} docs for more information.
* <p>{@inheritDoc}
*
* <p>A future update to Cds2Client will implement additional parts of the contact discovery
* protocol.
*/
public class Cds2Client extends SgxClient {
public Cds2Client(byte[] mrenclave, byte[] attestationMsg, Instant currentInstant)
throws AttestationDataException, AttestationFailedException {
throws AttestationDataException {
super(
filterExceptions(
AttestationDataException.class,
AttestationFailedException.class,
() ->
Native.Cds2ClientState_New(
mrenclave, attestationMsg, currentInstant.toEpochMilli())));

View File

@ -11,40 +11,34 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)">AES-256-CTR</a>
* stream cipher with a 12-byte nonce and an initial counter.
*
* <p>CTR mode is built on XOR, so encrypting and decrypting are the same operation.
*/
public class Aes256Ctr32 extends NativeHandleGuard.SimpleOwner {
public class Aes256Ctr32 implements NativeHandleGuard.Owner {
private final long unsafeHandle;
public Aes256Ctr32(byte[] key, byte[] nonce, int initialCtr) throws InvalidKeyException {
super(
this.unsafeHandle =
filterExceptions(
InvalidKeyException.class, () -> Native.Aes256Ctr32_New(key, nonce, initialCtr)));
InvalidKeyException.class, () -> Native.Aes256Ctr32_New(key, nonce, initialCtr));
}
@Override
protected void release(long nativeHandle) {
Native.Aes256Ctr32_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.Aes256Ctr32_Destroy(this.unsafeHandle);
}
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*/
public void process(byte[] data) {
this.process(data, 0, data.length);
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256Ctr32_Process(guard.nativeHandle(), data, 0, data.length);
}
}
/**
* Encrypts the plaintext, or decrypts the ciphertext, in {@code data}, in place, advancing the
* state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*/
public void process(byte[] data, int offset, int length) {
guardedRun((nativeHandle) -> Native.Aes256Ctr32_Process(nativeHandle, data, offset, length));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256Ctr32_Process(guard.nativeHandle(), data, offset, length);
}
}
}

View File

@ -11,72 +11,48 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow decrypting data without having it
* resident in memory all at once. You <strong>must</strong> call {@link #verifyTag} when the
* decryption is complete, or else you have no authenticity guarantees.
*
* @see Aes256GcmEncryption
*/
public class Aes256GcmDecryption extends NativeHandleGuard.SimpleOwner {
/** The size of the authentication tag, as used by {@link #verifyTag} */
public class Aes256GcmDecryption implements NativeHandleGuard.Owner {
public static final int TAG_SIZE_IN_BYTES = 16;
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter.
*/
private long unsafeHandle;
public Aes256GcmDecryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
this.unsafeHandle =
filterExceptions(
InvalidKeyException.class,
() -> Native.Aes256GcmDecryption_New(key, nonce, associatedData)));
() -> Native.Aes256GcmDecryption_New(key, nonce, associatedData));
}
@Override
protected void release(long nativeHandle) {
Native.Aes256GcmDecryption_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.Aes256GcmDecryption_Destroy(this.unsafeHandle);
}
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, 0, ciphertext.length));
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
/**
* Decrypts {@code ciphertext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #verifyTag} when decryption is complete.
*/
public void decrypt(byte[] ciphertext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmDecryption_Update(nativeHandle, ciphertext, offset, length));
public void decrypt(byte[] plaintext) {
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256GcmDecryption_Update(guard.nativeHandle(), plaintext, 0, plaintext.length);
}
}
public void decrypt(byte[] plaintext, int offset, int length) {
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256GcmDecryption_Update(guard.nativeHandle(), plaintext, offset, length);
}
}
/**
* Returns {@code true} if and only if {@code tag} matches the ciphertext that has been processed.
*
* <p>After calling {@code verifyTag}, this object may not be used anymore.
*/
public boolean verifyTag(byte[] tag) {
return guardedMap(
(nativeHandle) ->
filterExceptions(() -> Native.Aes256GcmDecryption_VerifyTag(nativeHandle, tag)));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
boolean tagOk =
filterExceptions(() -> Native.Aes256GcmDecryption_VerifyTag(guard.nativeHandle(), tag));
Native.Aes256GcmDecryption_Destroy(guard.nativeHandle());
this.unsafeHandle = 0;
return tagOk;
}
}
}

View File

@ -11,68 +11,45 @@ import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
/**
* Implements the <a
* href="https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Galois/counter_(GCM)">AES-256-GCM</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>This API exposes the streaming nature of AES-GCM to allow encrypting data without having it
* resident in memory all at once. You must call {@link #computeTag} when the encryption is complete
* to produce the authentication tag for the ciphertext, and then make sure the tag makes it to the
* decrypter.
*
* @see Aes256GcmDecryption
*/
public class Aes256GcmEncryption extends NativeHandleGuard.SimpleOwner {
/**
* Initializes the cipher with the given inputs.
*
* <p>The associated data is not included in the plaintext or tag; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*/
public class Aes256GcmEncryption implements NativeHandleGuard.Owner {
private long unsafeHandle;
public Aes256GcmEncryption(byte[] key, byte[] nonce, byte[] associatedData)
throws InvalidKeyException {
super(
this.unsafeHandle =
filterExceptions(
InvalidKeyException.class,
() -> Native.Aes256GcmEncryption_New(key, nonce, associatedData)));
() -> Native.Aes256GcmEncryption_New(key, nonce, associatedData));
}
@Override
protected void release(long nativeHandle) {
Native.Aes256GcmEncryption_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.Aes256GcmEncryption_Destroy(this.unsafeHandle);
}
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Bytes outside the designated offset/length are unchanged.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext, int offset, int length) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, offset, length));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256GcmEncryption_Update(guard.nativeHandle(), plaintext, offset, length);
}
}
/**
* Encrypts {@code plaintext} in place and advances the state of the cipher.
*
* <p>Don't forget to call {@link #computeTag} when encryption is complete.
*/
public void encrypt(byte[] plaintext) {
guardedRun(
(nativeHandle) ->
Native.Aes256GcmEncryption_Update(nativeHandle, plaintext, 0, plaintext.length));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.Aes256GcmEncryption_Update(guard.nativeHandle(), plaintext, 0, plaintext.length);
}
}
/**
* Produces an authentication tag for the plaintext that has been processed.
*
* <p>After calling {@code computeTag}, this object may not be used anymore.
*/
public byte[] computeTag() {
return guardedMap(Native::Aes256GcmEncryption_ComputeTag);
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
byte[] tag = Native.Aes256GcmEncryption_ComputeTag(guard.nativeHandle());
Native.Aes256GcmEncryption_Destroy(guard.nativeHandle());
this.unsafeHandle = 0;
return tag;
}
}
}

View File

@ -12,57 +12,40 @@ import org.signal.libsignal.internal.NativeHandleGuard;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.InvalidMessageException;
/**
* Implements the <a href="https://en.wikipedia.org/wiki/AES-GCM-SIV">AES-256-GCM-SIV</a>
* authenticated stream cipher with a 12-byte nonce.
*
* <p>AES-GCM-SIV is a multi-pass algorithm (to generate the "synthetic initialization vector"), so
* this API does not expose a streaming form.
*/
public class Aes256GcmSiv extends NativeHandleGuard.SimpleOwner {
class Aes256GcmSiv implements NativeHandleGuard.Owner {
private final long unsafeHandle;
public Aes256GcmSiv(byte[] key) throws InvalidKeyException {
super(filterExceptions(InvalidKeyException.class, () -> Native.Aes256GcmSiv_New(key)));
this.unsafeHandle =
filterExceptions(InvalidKeyException.class, () -> Native.Aes256GcmSiv_New(key));
}
@Override
protected void release(long nativeHandle) {
Native.Aes256GcmSiv_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.Aes256GcmSiv_Destroy(this.unsafeHandle);
}
/**
* Encrypts the given plaintext using the given nonce, and authenticating the ciphertext and given
* associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter. If you don't need any extra data, pass an empty array.
*
* @return The encrypted data, including an appended 16-byte authentication tag.
*/
public byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
return filterExceptions(
() ->
guardedMapChecked(
nativeHandle ->
Native.Aes256GcmSiv_Encrypt(nativeHandle, plaintext, nonce, associated_data)));
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
/**
* Decrypts the given ciphertext using the given nonce, and authenticating the ciphertext and
* given associated data.
*
* <p>The associated data is not included in the ciphertext; instead, it's expected to match
* between the encrypter and decrypter.
*
* @return The decrypted data
*/
public byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
byte[] encrypt(byte[] plaintext, byte[] nonce, byte[] associated_data) {
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return filterExceptions(
() ->
Native.Aes256GcmSiv_Encrypt(guard.nativeHandle(), plaintext, nonce, associated_data));
}
}
byte[] decrypt(byte[] ciphertext, byte[] nonce, byte[] associated_data)
throws InvalidMessageException {
return filterExceptions(
InvalidMessageException.class,
() ->
guardedMapChecked(
(nativeHandle) ->
Native.Aes256GcmSiv_Decrypt(nativeHandle, ciphertext, nonce, associated_data)));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return filterExceptions(
InvalidMessageException.class,
() ->
Native.Aes256GcmSiv_Decrypt(
guard.nativeHandle(), ciphertext, nonce, associated_data));
}
}
}

View File

@ -10,27 +10,38 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
public class CryptographicHash extends NativeHandleGuard.SimpleOwner {
public class CryptographicHash implements NativeHandleGuard.Owner {
private final long unsafeHandle;
public CryptographicHash(String algo) {
super(filterExceptions(() -> Native.CryptographicHash_New(algo)));
this.unsafeHandle = filterExceptions(() -> Native.CryptographicHash_New(algo));
}
public long unsafeNativeHandleWithoutGuard() {
return unsafeHandle;
}
@Override
protected void release(long nativeHandle) {
Native.CryptographicHash_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.CryptographicHash_Destroy(this.unsafeHandle);
}
public void update(byte[] input, int offset, int len) {
guardedRun(
(nativeHandle) ->
Native.CryptographicHash_UpdateWithOffset(nativeHandle, input, offset, len));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.CryptographicHash_UpdateWithOffset(guard.nativeHandle(), input, offset, len);
}
}
public void update(byte[] input) {
guardedRun((nativeHandle) -> Native.CryptographicHash_Update(nativeHandle, input));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.CryptographicHash_Update(guard.nativeHandle(), input);
}
}
public byte[] finish() {
return guardedMap(Native::CryptographicHash_Finalize);
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return Native.CryptographicHash_Finalize(guard.nativeHandle());
}
}
}

View File

@ -10,27 +10,38 @@ import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
import org.signal.libsignal.internal.Native;
import org.signal.libsignal.internal.NativeHandleGuard;
public class CryptographicMac extends NativeHandleGuard.SimpleOwner {
public class CryptographicMac implements NativeHandleGuard.Owner {
private final long unsafeHandle;
public CryptographicMac(String algo, byte[] key) {
super(filterExceptions(() -> Native.CryptographicMac_New(algo, key)));
this.unsafeHandle = filterExceptions(() -> Native.CryptographicMac_New(algo, key));
}
@Override
protected void release(long nativeHandle) {
Native.CryptographicMac_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.CryptographicMac_Destroy(this.unsafeHandle);
}
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
public void update(byte[] input, int offset, int len) {
guardedRun(
(nativeHandle) ->
Native.CryptographicMac_UpdateWithOffset(nativeHandle, input, offset, len));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.CryptographicMac_UpdateWithOffset(guard.nativeHandle(), input, offset, len);
}
}
public void update(byte[] input) {
guardedRun((nativeHandle) -> Native.CryptographicMac_Update(nativeHandle, input));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
Native.CryptographicMac_Update(guard.nativeHandle(), input);
}
}
public byte[] finish() {
return guardedMap(Native::CryptographicMac_Finalize);
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return Native.CryptographicMac_Finalize(guard.nativeHandle());
}
}
}

View File

@ -30,13 +30,10 @@ import org.signal.libsignal.internal.NativeHandleGuard;
* HsmEnclaveClient.establishedRecv(), which decrypts and verifies it, passing the plaintext back to
* the client for processing.
*/
public class HsmEnclaveClient extends NativeHandleGuard.SimpleOwner {
public class HsmEnclaveClient implements NativeHandleGuard.Owner {
private final long unsafeHandle;
public HsmEnclaveClient(byte[] public_key, List<byte[]> code_hashes) {
super(HsmEnclaveClient.createNativeFrom(public_key, code_hashes));
}
private static long createNativeFrom(byte[] public_key, List<byte[]> code_hashes) {
ByteArrayOutputStream concatHashes = new ByteArrayOutputStream();
for (byte[] hash : code_hashes) {
if (hash.length != 32) {
@ -48,52 +45,55 @@ public class HsmEnclaveClient extends NativeHandleGuard.SimpleOwner {
throw new AssertionError("writing to ByteArrayOutputStream failed", e);
}
}
return filterExceptions(
() -> Native.HsmEnclaveClient_New(public_key, concatHashes.toByteArray()));
this.unsafeHandle =
filterExceptions(() -> Native.HsmEnclaveClient_New(public_key, concatHashes.toByteArray()));
}
@Override
protected void release(long nativeHandle) {
Native.HsmEnclaveClient_Destroy(nativeHandle);
@SuppressWarnings("deprecation")
protected void finalize() {
Native.HsmEnclaveClient_Destroy(this.unsafeHandle);
}
public long unsafeNativeHandleWithoutGuard() {
return this.unsafeHandle;
}
/** Initial request to send to HSM enclave, to begin handshake. */
public byte[] initialRequest() {
return filterExceptions(() -> guardedMapChecked(Native::HsmEnclaveClient_InitialRequest));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return filterExceptions(() -> Native.HsmEnclaveClient_InitialRequest(guard.nativeHandle()));
}
}
/** Called by client upon receipt of first message from HSM enclave, to complete handshake. */
public void completeHandshake(byte[] handshakeResponse)
throws EnclaveCommunicationFailureException, TrustedCodeMismatchException {
filterExceptions(
EnclaveCommunicationFailureException.class,
TrustedCodeMismatchException.class,
() ->
guardedRunChecked(
(nativeHandle) ->
Native.HsmEnclaveClient_CompleteHandshake(nativeHandle, handshakeResponse)));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
filterExceptions(
EnclaveCommunicationFailureException.class,
TrustedCodeMismatchException.class,
() -> Native.HsmEnclaveClient_CompleteHandshake(guard.nativeHandle(), handshakeResponse));
}
}
/** Called by client after completeHandshake has succeeded, to encrypt a message to send. */
public byte[] establishedSend(byte[] plaintextToSend)
throws EnclaveCommunicationFailureException {
return filterExceptions(
EnclaveCommunicationFailureException.class,
() ->
guardedMapChecked(
(nativeHandle) ->
Native.HsmEnclaveClient_EstablishedSend(nativeHandle, plaintextToSend)));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return filterExceptions(
EnclaveCommunicationFailureException.class,
() -> Native.HsmEnclaveClient_EstablishedSend(guard.nativeHandle(), plaintextToSend));
}
}
/** Called by client after completeHandshake has succeeded, to decrypt a received message. */
public byte[] establishedRecv(byte[] receivedCiphertext)
throws EnclaveCommunicationFailureException {
return filterExceptions(
EnclaveCommunicationFailureException.class,
() ->
guardedMapChecked(
(nativeHandle) ->
Native.HsmEnclaveClient_EstablishedRecv(nativeHandle, receivedCiphertext)));
try (NativeHandleGuard guard = new NativeHandleGuard(this)) {
return filterExceptions(
EnclaveCommunicationFailureException.class,
() -> Native.HsmEnclaveClient_EstablishedRecv(guard.nativeHandle(), receivedCiphertext));
}
}
}

Some files were not shown because too many files have changed in this diff Show More