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