From 575aca5bfed7e9d660c76d2a2b2fbb515eaf01f7 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Wed, 4 Feb 2026 20:49:35 +0100 Subject: [PATCH] Add matrix testing for authorizer variants Run VSS integration tests against multiple server authorizer configurations (noop, sig, jwt). Each matrix job builds the vss-server with the appropriate flags and passes the corresponding cfg flag to the test runner. - Use release builds for vss-server - Pass authorizer-specific cfg flags for test differentiation - Add `test_utils` feature with JWT and signature-based header generation - Add RSA keypair fixtures for JWT signing - Update VSS tests to use auth-aware headers --- .github/workflows/vss-integration.yml | 23 +++++- Cargo.toml | 5 ++ src/io/mod.rs | 6 +- src/io/test_utils.rs | 110 ++++++++++++++++++++++++++ src/io/vss_store.rs | 26 +++--- tests/fixtures/vss_jwt_rsa_prv.pem | 28 +++++++ tests/fixtures/vss_jwt_rsa_pub.pem | 9 +++ tests/integration_tests_vss.rs | 19 ++--- 8 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 tests/fixtures/vss_jwt_rsa_prv.pem create mode 100644 tests/fixtures/vss_jwt_rsa_pub.pem diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index b5c4e9a0b..eccdb1574 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -9,6 +9,9 @@ concurrency: jobs: build-and-test: runs-on: ubuntu-latest + strategy: + matrix: + authorizer: [noop, sig, jwt] services: postgres: @@ -39,10 +42,22 @@ jobs: - name: Build and Deploy VSS Server run: | cd vss-server/rust - cargo run server/vss-server-config.toml& - - name: Run VSS Integration tests + if [ "${{ matrix.authorizer }}" == "noop" ]; then + RUSTFLAGS="--cfg noop_authorizer" cargo run --release --no-default-features server/vss-server-config.toml & + elif [ "${{ matrix.authorizer }}" == "jwt" ]; then + # Public key for testing purposes solely. + export VSS_JWT_RSA_PEM=$(cat ../../ldk-node/tests/fixtures/vss_jwt_rsa_pub.pem) + cargo run --release server/vss-server-config.toml & + elif [ "${{ matrix.authorizer }}" == "sig" ]; then + cargo run --release server/vss-server-config.toml & + else + echo "Unknown authorizer: ${{ matrix.authorizer }}" && exit 1 + fi + + - name: Run VSS Integration tests for ${{ matrix.authorizer }} run: | cd ldk-node export TEST_VSS_BASE_URL="http://localhost:8080/vss" - RUSTFLAGS="--cfg vss_test" cargo test io::vss_store - RUSTFLAGS="--cfg vss_test --cfg cycle_tests" cargo test --test integration_tests_vss + RUSTFLAGS="--cfg vss_test --cfg ${{ matrix.authorizer }}_auth_test" cargo test --features test_utils io::vss_store + RUSTFLAGS="--cfg vss_test --cfg cycle_tests --cfg ${{ matrix.authorizer }}_auth_test" cargo test \ + --features test_utils --test integration_tests_vss diff --git a/Cargo.toml b/Cargo.toml index 67e492185..49fae3409 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ panic = 'abort' # Abort on panic [features] default = [] +test_utils = ["jsonwebtoken", "lightning/_test_utils"] [dependencies] #lightning = { version = "0.2.0", features = ["std"] } @@ -79,6 +80,7 @@ vss-client = { package = "vss-client-ng", version = "0.4" } prost = { version = "0.11.6", default-features = false} #bitcoin-payment-instructions = { version = "0.6" } bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "e4d519b95b26916dc6efa22f8f1cc11a818ce7a7" } +jsonwebtoken = { version = "9.3.0", optional = true, default-features = false, features = ["use_pem"] } [target.'cfg(windows)'.dependencies] winapi = { version = "0.3", features = ["winbase"] } @@ -123,6 +125,9 @@ check-cfg = [ "cfg(cln_test)", "cfg(lnd_test)", "cfg(cycle_tests)", + "cfg(noop_auth_test)", + "cfg(jwt_auth_test)", + "cfg(sig_auth_test)", ] [[bench]] diff --git a/src/io/mod.rs b/src/io/mod.rs index e080d39f7..b3fdd2f8f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -8,8 +8,12 @@ //! Objects and traits for data persistence. pub mod sqlite_store; -#[cfg(test)] +#[cfg(any(test, feature = "test_utils"))] pub(crate) mod test_utils; + +#[cfg(feature = "test_utils")] +pub use test_utils::get_fixed_headers; + pub(crate) mod utils; pub mod vss_store; diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index cbcd90d29..349881efe 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -5,6 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +#![allow(dead_code)] + use std::collections::{hash_map, HashMap}; use std::future::Future; use std::panic::RefUnwindSafe; @@ -350,3 +352,111 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { // Make sure everything is persisted as expected after close. check_persisted_data!(persister_0_max_pending_updates * 2 * EXPECTED_UPDATES_PER_PAYMENT + 1); } + +#[cfg(all(feature = "test_utils", jwt_auth_test))] +mod jwt_auth { + use super::*; + + use std::time::SystemTime; + + use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; + use serde::{Deserialize, Serialize}; + + // Private key for testing purposes solely. + const VSS_PRIVATE_PEM: &str = include_str!("../../tests/fixtures/vss_jwt_rsa_prv.pem"); + + #[derive(Serialize, Deserialize)] + struct TestClaims { + sub: String, + iat: i64, + nbf: i64, + exp: i64, + } + + pub fn generate_test_jwt(private_pem: &str, user_id: &str) -> String { + let now = + SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64; + + let claims = + TestClaims { sub: user_id.to_owned(), iat: now, nbf: now, exp: now + (60 * 10) }; + + let encoding_key = EncodingKey::from_rsa_pem(private_pem.as_bytes()) + .expect("Failed to create EncodingKey"); + + encode(&Header::new(Algorithm::RS256), &claims, &encoding_key).unwrap() + } + + pub fn get_fixed_headers() -> HashMap { + let token = generate_test_jwt(VSS_PRIVATE_PEM, "test"); + let mut headers = HashMap::new(); + headers.insert("Authorization".to_string(), format!("Bearer {}", token)); + return headers; + } +} + +#[cfg(all(feature = "test_utils", sig_auth_test))] +mod sig_auth { + use super::*; + + use std::time::SystemTime; + use std::time::UNIX_EPOCH; + + use bitcoin::hashes::sha256::Hash; + use bitcoin::hashes::Hash as _; + use bitcoin::secp256k1::{self, SecretKey}; + + use crate::hex_utils; + + // Must match vss-server's SignatureAuthorizer constant. + // See: https://github.com/lightningdevkit/vss-server/blob/main/rust/auth-impls/src/signature.rs#L21 + const SIGNING_CONSTANT: &'static [u8] = + b"VSS Signature Authorizer Signing Salt Constant.................."; + + fn build_auth_token(secret_key: &SecretKey) -> String { + let secp = secp256k1::Secp256k1::new(); + let pubkey = secret_key.public_key(&secp); + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + + let mut bytes_to_sign = Vec::new(); + bytes_to_sign.extend_from_slice(SIGNING_CONSTANT); + bytes_to_sign.extend_from_slice(&pubkey.serialize()); + bytes_to_sign.extend_from_slice(format!("{now}").as_bytes()); + + let hash = Hash::hash(&bytes_to_sign); + let msg = secp256k1::Message::from_digest(hash.to_byte_array()); + let sig = secp.sign_ecdsa(&msg, &secret_key); + + format!("{pubkey:x}{}{now}", hex_utils::to_string(&sig.serialize_compact())) + } + + pub fn get_fixed_headers() -> HashMap { + let secret_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let token = build_auth_token(&secret_key); + let mut headers = HashMap::new(); + headers.insert("Authorization".to_string(), token); + return headers; + } +} + +/// Returns a hashmap of fixed headers, where, depending on configuration, +/// corresponds to valid headers for no-op, signature-based, or jwt-based +/// authorizers on vss-server. +pub fn get_fixed_headers() -> HashMap { + #[cfg(noop_auth_test)] + { + HashMap::new() + } + + #[cfg(all(jwt_auth_test, feature = "test_utils"))] + { + jwt_auth::get_fixed_headers() + } + + #[cfg(all(feature = "test_utils", sig_auth_test))] + { + sig_auth::get_fixed_headers() + } + + #[cfg(not(any(noop_auth_test, all(jwt_auth_test, feature = "test_utils"), sig_auth_test)))] + HashMap::new() +} diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index b4fdc770a..3f0c19ac2 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -962,14 +962,12 @@ impl VssStoreBuilder { #[cfg(test)] #[cfg(vss_test)] mod tests { - use std::collections::HashMap; - use rand::distr::Alphanumeric; use rand::{rng, Rng, RngCore}; use vss_client::headers::FixedHeaders; use super::*; - use crate::io::test_utils::do_read_write_remove_list_persist; + use crate::io::test_utils::{do_read_write_remove_list_persist, get_fixed_headers}; #[test] fn vss_read_write_remove_list_persist() { @@ -978,9 +976,14 @@ mod tests { let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); let mut vss_seed = [0u8; 32]; rng.fill_bytes(&mut vss_seed); - let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); - let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + let header_provider = get_fixed_headers(); + let vss_store = VssStore::new( + vss_base_url, + rand_store_id, + vss_seed, + Arc::new(FixedHeaders::new(header_provider)), + ) + .unwrap(); do_read_write_remove_list_persist(&vss_store); } @@ -991,9 +994,14 @@ mod tests { let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); let mut vss_seed = [0u8; 32]; rng.fill_bytes(&mut vss_seed); - let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); - let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + let header_provider = get_fixed_headers(); + let vss_store = VssStore::new( + vss_base_url, + rand_store_id, + vss_seed, + Arc::new(FixedHeaders::new(header_provider)), + ) + .unwrap(); do_read_write_remove_list_persist(&vss_store); drop(vss_store) diff --git a/tests/fixtures/vss_jwt_rsa_prv.pem b/tests/fixtures/vss_jwt_rsa_prv.pem new file mode 100644 index 000000000..5bc08b4cc --- /dev/null +++ b/tests/fixtures/vss_jwt_rsa_prv.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCuIYJJ1dzNmuct +fzj+W4EeJXic/A6gkHJOS7MHqAqOqMg49aZfOj6y4kmhU3/fal5OccJ4299ohSnJ +rMKwWoL5YHYD1Y742Ez/trpjETTV6CwjhLovCVtWbZrjivUnYb1fEeGoQ8p+COOK +yMM3s/iQVlhzqo86kZ9fcaGFizrykcRimSDOuN3V/AWcB3fcKufLS76z6ysNTwiK +X/wnBUEtofALIV1i9rcyFpumRGi/9lo80eVcC1TErxA3C5IEwJQ18XCatM2Hf2xi +4D35JDK80/0MY3u7TJgYrFpaThuKd3lnSSPx51enJUEaNgXuUNvTdKkFEn1ASRi5 +LNbNLKhXAgMBAAECggEATokCcDaqjXjNxzFYDTBL/cK8sWDlX/mF9FYj+tIJYOoy +063HSa/FU3zH5KD6TVN2ET8xjLzt+AAHJtRqQouwArVExNnuz8EOiU5qpf++qrM6 +JRLZvhkkPsjUUMf9ZbOpa1VvRyq8CzgLGC8QDPF4q/ClmBVW3/2JucxQIyD2hywE +MDc8on3nEvCzMGSxUm2EIQn30iF8W2WZyNLk4RK+UUOUHGsFUN0PRfsHFQCAar0V +ZnSxlJgDKETXGjkX8G+H+EyPd+oFH3QtZFTzxk6ghLEZV1vetqMI9WEFnMLKfmFW +tBGYuGcq+72G5mlJqlnix69A93SJziymlS/QWgKrQQKBgQDki5E++NoUyslKMZca +yYWokqxs6T0y15Cig7lDuRfcfDNDjbf9AkRpUZ0O9VESWhCiRG0NKUbHVlAlAb1h +EbWPc2QCs+Tzt2uqeavohbVb9NMsEFSKgx8oUuIisAgUo9Xui+80A+7s54PoeqnT +ULE7KYCbs5oMJjGwpuiolYFYpwKBgQDDDIsEyxdloE6MxpwRshYKzOWR0ADeKuIf +kwEpF1ap5Ng0PdAvIfQS/aRpeb/Tx8Uv2+YMTwX2RyvgbMBFjJa3TEqagFcS4Chu +tabLuyjMq9LlUQdsh4xqfGFF3vaT9coDpbzNvHcmLkK6gnrbnG6mG06xFybqBQ8I +k8dPpnN40QKBgQDUBqc5RKUNpRQZQOhucYcOXQSaBchA4rvMCWhW6+C3LIJiqZeH +ohLVomGS/wO3gtbrs494JlMDm4++xV5sL4HBE8w0tbAyanf4L+jMTz9xkDBZMM09 +s2e0gTBJ/gWBIH3YUPoZx4xhPGejxijHYpUJzfcCfBzuKIDw4ef2fr0BAQKBgEcB +X/KExKW4cCALhXFjtWaFJOWqJUa7scnwyDFfT6tVpeeOwSUHZUUslRfYvJ6qUPyV +PvAoLHF1g2GV9YDcJ1nfKiGIqyox9EYpVuk/3yBzRLk6gEtgJRv236qB+p3uknY1 +dcAn5fA+Uwh2y6b7EcTimAkb9oym/swOkDZM0CihAoGBAJ9W91zU9H5rMBwiWMKP +ppReTRxxN8oJNk0Cirxr58YHQNXtGWkno316/SPJZzML29c8+QAoJ8uatwzaZzt9 +S6Cq2bYEyO7LPqs3SLRrK802QGvV7Y4P2rX4pjYOMM9qddOnT+qkVyyqVguazfDJ +xrhmGsrdBu3nBkwwpCBps6KZ +-----END PRIVATE KEY----- diff --git a/tests/fixtures/vss_jwt_rsa_pub.pem b/tests/fixtures/vss_jwt_rsa_pub.pem new file mode 100644 index 000000000..3e6f5e71f --- /dev/null +++ b/tests/fixtures/vss_jwt_rsa_pub.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAriGCSdXczZrnLX84/luB +HiV4nPwOoJByTkuzB6gKjqjIOPWmXzo+suJJoVN/32peTnHCeNvfaIUpyazCsFqC ++WB2A9WO+NhM/7a6YxE01egsI4S6LwlbVm2a44r1J2G9XxHhqEPKfgjjisjDN7P4 +kFZYc6qPOpGfX3GhhYs68pHEYpkgzrjd1fwFnAd33Crny0u+s+srDU8Iil/8JwVB +LaHwCyFdYva3MhabpkRov/ZaPNHlXAtUxK8QNwuSBMCUNfFwmrTNh39sYuA9+SQy +vNP9DGN7u0yYGKxaWk4bind5Z0kj8edXpyVBGjYF7lDb03SpBRJ9QEkYuSzWzSyo +VwIDAQAB +-----END PUBLIC KEY----- diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 54912b358..a9df2f424 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -5,15 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -#![cfg(vss_test)] +#![cfg(all(vss_test, feature = "test_utils"))] mod common; -use std::collections::HashMap; +use rand::{rng, Rng}; use ldk_node::entropy::NodeEntropy; +use ldk_node::io::get_fixed_headers; use ldk_node::Builder; -use rand::{rng, Rng}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_with_vss_store() { @@ -29,7 +29,7 @@ async fn channel_full_cycle_with_vss_store() { config_a.node_entropy, vss_base_url.clone(), "node_1_store".to_string(), - HashMap::new(), + get_fixed_headers(), ) .unwrap(); node_a.start().unwrap(); @@ -43,7 +43,7 @@ async fn channel_full_cycle_with_vss_store() { config_b.node_entropy, vss_base_url, "node_2_store".to_string(), - HashMap::new(), + get_fixed_headers(), ) .unwrap(); node_b.start().unwrap(); @@ -84,7 +84,7 @@ async fn vss_v0_schema_backwards_compatibility() { .build_with_vss_store_and_fixed_headers( vss_base_url.clone(), store_id.clone(), - HashMap::new(), + get_fixed_headers(), ) .unwrap(); @@ -123,7 +123,7 @@ async fn vss_v0_schema_backwards_compatibility() { node_entropy, vss_base_url, store_id, - HashMap::new(), + get_fixed_headers(), ) .unwrap(); @@ -158,12 +158,13 @@ async fn vss_node_restart() { builder.set_network(bitcoin::Network::Regtest); builder.set_storage_dir_path(storage_path.clone()); builder.set_chain_source_esplora(esplora_url.clone(), None); + let node = builder .build_with_vss_store_and_fixed_headers( node_entropy, vss_base_url.clone(), store_id.clone(), - HashMap::new(), + get_fixed_headers(), ) .unwrap(); @@ -197,7 +198,7 @@ async fn vss_node_restart() { node_entropy, vss_base_url, store_id, - HashMap::new(), + get_fixed_headers(), ) .unwrap();