3 changed files with 246 additions and 0 deletions
@ -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 |
|||
@ -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() |
|||
@ -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…
Reference in new issue