Browse Source

Add a manual workflow that runs the SDK live integration tests

v2-4-sdk-live-tests
Daniel García 6 hours ago
parent
commit
76b2905aa2
No known key found for this signature in database GPG Key ID: FC8A7D14C3CD543A
  1. 65
      .github/workflows/sdk-live-tests.yml
  2. 120
      tools/sdk-live-tests/register_vector.py
  3. 61
      tools/sdk-live-tests/run.sh

65
.github/workflows/sdk-live-tests.yml

@ -0,0 +1,65 @@
name: SDK live tests
permissions: {}
# Runs the live-server integration tests of bitwarden/sdk-internal against this branch.
# Only started by hand: it builds the SDK too, and the tests aren't in a released SDK yet.
on:
workflow_dispatch:
inputs:
sdk_ref:
description: "Branch, tag or commit of bitwarden/sdk-internal to test with"
required: true
default: "km/live-server-integration-tests"
defaults:
run:
shell: bash
jobs:
sdk-live-tests:
name: SDK live tests
runs-on: ubuntu-24.04
timeout-minutes: 90
steps:
- name: "Install dependencies Ubuntu"
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends build-essential libssl-dev pkg-config
- name: "Checkout"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Checkout sdk-internal"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: bitwarden/sdk-internal
ref: ${{ inputs.sdk_ref }}
path: sdk-internal
persist-credentials: false
# Each checkout pins its own toolchain in rust-toolchain.toml
- name: "Install toolchains"
run: |
rustup toolchain install
cd sdk-internal
rustup toolchain install
rustup target add wasm32-unknown-unknown
rustup component add rust-src
- name: "Setup Node"
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
- name: "Install binaryen"
run: npm i -g binaryen
- name: "Run the SDK live tests"
run: tools/sdk-live-tests/run.sh --sdk-dir sdk-internal
- name: "Upload logs"
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sdk-live-tests-logs
path: target/sdk-live-tests/**/*.log

120
tools/sdk-live-tests/register_vector.py

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Registers the account of an sdk-internal test vector on a vaultwarden server, as a client would.
Usage: register_vector.py <vector.json> <server-url>
A V1 vector is registered with the flat `keys` object, a V2 one with `accountKeys` and the user key
id. Prints the account's email and password on two lines, for the caller to log in with.
Exits with SKIP, and the reason on stderr, for a vector the live tests can't use.
"""
import json
import sys
import urllib.error
import urllib.request
SKIP = 3
MIN_PBKDF2_ITERATIONS = 100_000
def skip_reason(vector):
if not any("masterPasswordUnlock" in m for m in vector["unlockMethods"]):
return "it has no master password, which the live tests log in with"
pbkdf2 = vector["account"]["kdf"].get("pBKDF2")
if pbkdf2 is not None and pbkdf2["iterations"] < MIN_PBKDF2_ITERATIONS:
return f"registration requires at least {MIN_PBKDF2_ITERATIONS} PBKDF2 iterations (upstream 600000)"
return None
def kdf_of(account):
kind, params = next(iter(account["kdf"].items()))
if kind == "pBKDF2":
return {"kdfType": 0, "iterations": params["iterations"]}
return {
"kdfType": 1,
"iterations": params["iterations"],
"memory": params["memory"],
"parallelism": params["parallelism"],
}
def register_body(vector):
account = vector["account"]
raw = vector["rawCryptographicState"]
version, state = next(iter(account["accountCryptographicState"].items()))
kdf = kdf_of(account)
unlock = next(m["masterPasswordUnlock"] for m in vector["unlockMethods"] if "masterPasswordUnlock" in m)
mp_unlock = unlock["master_password_unlock"]
body = {
"email": account["email"],
"name": vector["name"],
"masterPasswordHint": None,
"masterPasswordAuthentication": {
"kdf": kdf,
"salt": mp_unlock["salt"],
"masterPasswordAuthenticationHash": vector["masterPasswordAuthenticationHash"],
},
"masterPasswordUnlock": {
"kdf": kdf,
"salt": mp_unlock["salt"],
"masterKeyWrappedUserKey": mp_unlock["masterKeyWrappedUserKey"],
},
}
if version == "V2":
body["masterPasswordUnlock"]["containedKeyId"] = raw["userKeyId"]
body["accountKeys"] = {
"userKeyEncryptedAccountPrivateKey": state["private_key"],
"accountPublicKey": raw["publicKey"],
"publicKeyEncryptionKeyPair": {
"wrappedPrivateKey": state["private_key"],
"publicKey": raw["publicKey"],
"signedPublicKey": state["signed_public_key"],
},
"signatureKeyPair": {
"signatureAlgorithm": "ed25519",
"wrappedSigningKey": state["signing_key"],
"verifyingKey": raw["verifyingKey"],
},
"securityState": {
"securityState": state["security_state"],
"securityVersion": account["securityVersion"],
},
}
else:
body["keys"] = {"encryptedPrivateKey": state["private_key"], "publicKey": raw["publicKey"]}
return body, unlock["password"]
def main():
with open(sys.argv[1]) as f:
vector = json.load(f)
server = sys.argv[2].rstrip("/")
reason = skip_reason(vector)
if reason is not None:
print(f"Skipped: {reason}", file=sys.stderr)
sys.exit(SKIP)
body, password = register_body(vector)
request = urllib.request.Request(
f"{server}/identity/accounts/register",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request):
pass
except urllib.error.HTTPError as e:
sys.exit(f"Registering {body['email']} failed: {e.code} {e.read().decode()}")
print(body["email"])
print(password)
if __name__ == "__main__":
main()

61
tools/sdk-live-tests/run.sh

@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Runs the live-server integration tests of bitwarden/sdk-internal against vaultwarden.
#
# Usage: tools/sdk-live-tests/run.sh --sdk-dir <path> [--skip-sdk-build]
#
# Every test vector of the SDK (test-vectors/users/) that the tests can log in with gets a fresh
# server and database, since the tests rotate the account's keys. Stops at the first failure; the
# server logs are in target/sdk-live-tests/. Building the SDK needs its Rust toolchain with the
# wasm32-unknown-unknown target and rust-src, Node.js, and binaryen (`npm i -g binaryen`).
set -euo pipefail
VW_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
OUT_DIR="${VW_DIR}/target/sdk-live-tests"
URL="http://127.0.0.1:8099"
SKIP=3 # register_vector.py's exit code for a vector the tests can't use
SDK_DIR=""
BUILD_SDK=1
while [[ $# -gt 0 ]]; do
case "$1" in
--sdk-dir) SDK_DIR="$(cd "$2" && pwd)"; shift 2 ;;
--skip-sdk-build) BUILD_SDK=0; shift ;;
*) echo "Unknown argument: $1" >&2; exit 2 ;;
esac
done
[[ -n "${SDK_DIR}" ]] || { echo "--sdk-dir is required" >&2; exit 2; }
TESTS_DIR="${SDK_DIR}/crates/bitwarden-wasm-internal/integration-tests"
(cd "${VW_DIR}" && cargo build --features sqlite)
if [[ ${BUILD_SDK} -eq 1 ]]; then
bash "${SDK_DIR}/crates/bitwarden-wasm-internal/build.sh"
(cd "${TESTS_DIR}" && npm ci)
fi
trap 'kill $(jobs -p) 2>/dev/null || true' EXIT
for vector_file in "${SDK_DIR}"/test-vectors/users/*.json; do
vector="$(basename "${vector_file}" .json)"
echo "=== ${vector}"
data_dir="${OUT_DIR}/${vector}"
rm -rf "${data_dir}" && mkdir -p "${data_dir}"
DATA_FOLDER="${data_dir}" DATABASE_URL="sqlite://${data_dir}/db.sqlite3" \
ROCKET_ADDRESS=127.0.0.1 ROCKET_PORT=8099 DOMAIN="${URL}" WEB_VAULT_ENABLED=false \
SIGNUPS_ALLOWED=true SIGNUPS_VERIFY=false LOGIN_RATELIMIT_MAX_BURST=1000 \
"${VW_DIR}/target/debug/vaultwarden" > "${data_dir}/vaultwarden.log" 2>&1 &
server=$!
curl -sf --retry 30 --retry-connrefused --retry-delay 1 "${URL}/alive" > /dev/null
status=0
credentials="$(python3 "${VW_DIR}/tools/sdk-live-tests/register_vector.py" "${vector_file}" "${URL}")" || status=$?
if [[ ${status} -eq 0 ]]; then
(cd "${TESTS_DIR}" && BW_LIVE_SERVER_URL="${URL}" BW_LIVE_EMAIL="$(sed -n 1p <<< "${credentials}")" \
BW_LIVE_PASSWORD="$(sed -n 2p <<< "${credentials}")" npm run test:live)
elif [[ ${status} -ne ${SKIP} ]]; then
exit "${status}"
fi
kill "${server}" && wait "${server}" || true
done
echo "All test vectors passed"
Loading…
Cancel
Save