diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..504da303 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# 0.1.0-alpha.0 - Jun. 4, 2026 + +- VSS service implementing VSS protocol version 0. (#34, #35) +- Rust workspace for the VSS server, API contract types, authentication implementations, and storage backend + implementations. (#34, #35, #43, #72, #79, #101) +- PostgreSQL storage backend with database initialization, migrations, TLS support, key-level versioning, store-level + global versioning, transactional writes, deletes, and paginated key-version listing. (#35, #55, #67, #96) +- Signature-based and JWT-based authorization implementations, with cfg-gated no-op authorization for local development + and tests. (#34, #43, #72, #79, #87) +- Configuration through TOML file and environment variables, including bind address, request body size, logging, JWT + RSA public key, and PostgreSQL settings. (#46, #67, #72, #73, #76, #87) +- Server logging to stdout/stderr and file, with SIGHUP log-file reopening and shutdown on CTRL-C/SIGTERM. (#34, #87) +- Prometheus-compatible `/metrics` health metric. (#99) +- Docker and Docker Compose files for local deployment. (#76, #80) +- Getting-started documentation. (#102) diff --git a/Cargo.lock b/Cargo.lock index 2b9dd589..0e1b23e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,7 +28,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "api" -version = "0.1.0" +version = "0.1.0-alpha.0" dependencies = [ "async-trait", "bytes", @@ -62,7 +62,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auth-impls" -version = "0.1.0" +version = "0.1.0-alpha.0" dependencies = [ "api", "async-trait", @@ -220,9 +220,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" @@ -608,7 +608,7 @@ dependencies = [ [[package]] name = "impls" -version = "0.1.0" +version = "0.1.0-alpha.0" dependencies = [ "api", "async-trait", @@ -1785,7 +1785,7 @@ dependencies = [ [[package]] name = "vss-server" -version = "0.1.0" +version = "0.1.0-alpha.0" dependencies = [ "api", "auth-impls", diff --git a/Cargo.toml b/Cargo.toml index 899bd267..32de9612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = ["server", "api", "impls", "auth-impls"] default-members = ["server"] [workspace.package] +version = "0.1.0-alpha.0" rust-version = "1.85.0" [profile.release] diff --git a/README.md b/README.md index 90af9e2a..bfa9d945 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,12 @@ VSS is also integrated with [LDK-node] v0.4.x as alpha support. ### Development -* **Build & Deploy**: Refer to [docs/getting-started.md](docs/getting-started.md) for instructions related to - building and deploying VSS. +* **Build & Deploy**: Refer to [docs/getting-started.md](docs/getting-started.md) for concrete PostgreSQL setup, + authentication configuration, local no-auth development mode, and deployment commands. * **Hosting**: VSS can either be self-hosted or deployed in the cloud. If a service provider is hosting VSS for multiple users, it must be configured with **HTTPS**, **Authentication/Authorization**, and **rate-limiting**. * **Authentication and Authorization**: VSS supports authentication via - [Proof-of-Private-Key-Knowledge](#Authentication) or [JWT](https://datatracker.ietf.org/doc/html/rfc7519). + [Proof-of-Private-Key-Knowledge](#authentication) or [JWT](https://datatracker.ietf.org/doc/html/rfc7519). The API also offers hooks for simple HTTP header-based authentication. Note that the security of authentication heavily relies on using HTTPS for all requests. * **Scaling**: VSS itself is stateless and can be horizontally scaled easily. VSS can be configured to point to a @@ -99,18 +99,19 @@ VSS is also integrated with [LDK-node] v0.4.x as alpha support. ### Authentication -By default, VSS uses a simple authentication scheme whereby each client must provide a valid signature for a -client-specified public key. The public key identifies the storage that belongs to the client. This scheme does -not impose **any** restrictions on who can interact with VSS; it **only** ensures that each client can only -access *their own* storage. Therefore, this scheme **must** be paired with a network-level gatekeeper to prevent -unauthorized interactions with VSS. +Default builds include signature and JWT authentication. If `jwt_auth_config.rsa_pem` or `VSS_JWT_RSA_PEM` is +configured, VSS verifies RS256 bearer tokens from the HTTP `Authorization` header. The JWT `sub` claim identifies +the storage user. VSS only implements token verification; operators must provide their own token issuance service. -The other option offered is JWT authentication. This form of authentication validates whether a client should -be given access to VSS, *and* which storage the client has access to. VSS only implements the verification half of this -scheme, and users must provide their own JWT issuance service if this solution is chosen. +If JWT is not configured, VSS uses signature authentication. Each client provides a valid signature for a +client-specified secp256k1 public key in the HTTP `Authorization` header. The public key identifies that client's +storage. This scheme does not impose **any** restrictions on who can interact with VSS; it **only** ensures that each +client can only access *their own* storage. Therefore, this scheme **must** be paired with a network-level gatekeeper +and rate limiting to prevent unauthorized interactions with VSS. -Finally, there is an option to completely disable all forms of authentication to VSS. This option should *only* be -used in local development and testing. +Finally, there is a cfg-gated option to disable all forms of authentication for local development and testing. Do not +publicly expose no-auth builds. See [docs/getting-started.md](docs/getting-started.md#authentication-setup) for exact +config and build commands. ### Summary diff --git a/api/Cargo.toml b/api/Cargo.toml index b34be8d7..3607c8c4 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "api" -version = "0.1.0" +version.workspace = true edition = "2021" rust-version.workspace = true diff --git a/auth-impls/Cargo.toml b/auth-impls/Cargo.toml index 5f956678..c87d6f39 100644 --- a/auth-impls/Cargo.toml +++ b/auth-impls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "auth-impls" -version = "0.1.0" +version.workspace = true edition = "2021" rust-version.workspace = true diff --git a/docs/getting-started.md b/docs/getting-started.md index 3b2a4d77..36d5a870 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,42 +1,145 @@ -# Versioned Storage Service (Rust) +# Getting Started -### Prerequisites +This guide covers a local source build and the required PostgreSQL and authentication setup. For +the threat model and auth overview, see the root [README](../README.md). -- Install Rust and Cargo (https://www.rust-lang.org/tools/install). -- Install PostgreSQL 15 (https://www.postgresql.org/download/) -- Install OpenSSL (used for TLS connections to the PostgreSQL backend: https://docs.rs/openssl/latest/openssl/#automatic) +## Prerequisites -### Building +- Rust and Cargo, using at least the repository MSRV of 1.85.0. +- PostgreSQL 15 or newer. +- OpenSSL development/runtime libraries for PostgreSQL TLS support. -``` -git clone https://github.com/lightningdevkit/vss-server.git -cd vss-server +## Quick Start with Docker PostgreSQL + +Start only the PostgreSQL service from the included Compose file: +```bash +docker compose up -d postgres cargo build --release +cargo run -- server/vss-server-config.toml +``` + +The sample config connects to `postgres:postgres@127.0.0.1:5432`, creates the `vss` database if it +does not exist, and runs schema migrations on startup. The VSS endpoint is +`http://localhost:8080/vss`; `/metrics` is available without VSS authentication: + +```bash +curl -f http://localhost:8080/metrics +``` + +The default `cargo run` command starts the server with signature authentication, because the sample +config does not include a JWT key. VSS API requests still need a valid `Authorization` header. To +exercise VSS API endpoints without auth headers in a local-only environment, use +[Local No-Auth Mode](#local-no-auth-mode). + +To run both PostgreSQL and the server in containers: + +```bash +docker compose up --build ``` -### Running -1. **Edit Configuration**: Modify `./server/vss-server-config.toml` to set application configuration and - environment variables as needed. -2. VSS will setup a PostgreSQL database on first launch if it is not found. You can also manually create the database - using the statement at `./impls/src/postgres/sql/v0_create_vss_db.sql`. -3. Start server: - ``` - cargo run server/vss-server-config.toml - ``` -4. VSS endpoint should be reachable at `http://localhost:8080/vss`. +## PostgreSQL Setup + +VSS first connects to `default_database`, then creates `vss_database` if it is missing, then connects +to `vss_database` and applies migrations. With the sample config these are `postgres` and `vss`. + +The configured PostgreSQL role must be able to connect to `default_database`. It must also either: + +- have permission to run `CREATE DATABASE vss` when `vss_database` does not exist, or +- use an already-created `vss_database` and have privileges to create/alter tables, indexes, + sequences, and rows in that database. + +For an existing local PostgreSQL instance, create the database yourself if the VSS user cannot create +databases: + +```bash +createdb -U postgres vss +``` + +Then update `[postgresql_config]` in `server/vss-server-config.toml` or set the corresponding +environment variables: + +```bash +VSS_PSQL_USERNAME=postgres +VSS_PSQL_PASSWORD=postgres +VSS_PSQL_ADDRESS=127.0.0.1:5432 +VSS_PSQL_DEFAULT_DB=postgres +VSS_PSQL_VSS_DB=vss +``` + +## Authentication Setup + +Default builds include both `jwt` and `sigs` features. At startup, VSS uses JWT auth if an RSA public +key is configured; otherwise it uses signature auth. Both schemes read the HTTP `Authorization` +header. + +### Signature Auth + +No TOML setting is required. Leave `[jwt_auth_config]` unset and the default build will require each +request to include a proof of private key knowledge in `Authorization`. The proof is the compressed +secp256k1 public key hex, followed by a compact ECDSA signature hex, followed by a Unix timestamp. +The signature covers VSS's signing constant, the public key, and the timestamp. Signature auth +isolates storage by public key, but does not decide who may create a new storage identity; +production deployments still need HTTPS, rate limiting, and an external access control layer. + +### JWT Auth + +Configure the RSA public key used to verify RS256 JWTs: + +```toml +[jwt_auth_config] +rsa_pem = """ +-----BEGIN PUBLIC KEY----- +... +-----END PUBLIC KEY----- +""" +``` + +or set `VSS_JWT_RSA_PEM`. Clients must send `Authorization: Bearer `. Tokens must be RS256, +include `sub` and `exp` claims, and omit `aud`; `sub` becomes the VSS storage user token. VSS only +verifies tokens, you must run the service that issues them. + +### Local No-Auth Mode + +For local development only, build with the cfg-gated no-op authorizer: + +```bash +RUSTFLAGS="--cfg noop_authorizer" cargo run --no-default-features -- server/vss-server-config.toml +``` + +Do not expose this mode outside a local test environment. + +## Configuration Reference + +Default builds read the following settings. Each listed TOML option can be overridden by its +environment variable. + +| TOML setting | Environment variable | Purpose | +| --- | --- | --- | +| `server_config.bind_address` | `VSS_BIND_ADDRESS` | HTTP listen address, for example `127.0.0.1:8080`. | +| `server_config.max_request_body_size` | `VSS_MAX_REQUEST_BODY_SIZE` | Request body limit in bytes. Defaults to 1 GiB. | +| `jwt_auth_config.rsa_pem` | `VSS_JWT_RSA_PEM` | RSA public key for JWT verification. Requires the `jwt` feature, which is enabled by default. | +| `postgresql_config.username` | `VSS_PSQL_USERNAME` | PostgreSQL user. | +| `postgresql_config.password` | `VSS_PSQL_PASSWORD` | PostgreSQL password. | +| `postgresql_config.address` | `VSS_PSQL_ADDRESS` | PostgreSQL host and port. | +| `postgresql_config.default_database` | `VSS_PSQL_DEFAULT_DB` | Database used for startup and database creation. | +| `postgresql_config.vss_database` | `VSS_PSQL_VSS_DB` | VSS application database. | +| `postgresql_config.tls` | `VSS_PSQL_TLS` | Enables PostgreSQL TLS with system trust roots. | +| `postgresql_config.tls.crt_pem` | `VSS_PSQL_CRT_PEM` | Adds a PEM root certificate and enables PostgreSQL TLS. | +| `log_config.level` | `VSS_LOG_LEVEL` | Log level. Defaults to `debug`. | +| `log_config.file` | `VSS_LOG_FILE` | Log file path. Defaults to `vss.log`. | -### Configuration +## Production Notes -Refer to `./server/vss-server-config.toml` to see available configuration options. +VSS is stateless and can be run behind a load balancer, but PostgreSQL is the durable state store. +Internet-facing deployments must terminate HTTPS in front of VSS, configure authentication, and add +rate limiting. -### Support +## Support -If you encounter any issues or have questions, feel free to open an issue on -the [GitHub repository](https://github.com/lightningdevkit/vss-server/issues). For further assistance or to discuss the -development of VSS, you can reach out to us in the [LDK Discord](https://discord.gg/5AcknnMfBw) in the `#vss` channel. +Open issues in the [GitHub repository](https://github.com/lightningdevkit/vss-server/issues), or use +the [LDK Discord](https://discord.gg/5AcknnMfBw) `#vss` channel. -[LDK Discord]: https://discord.gg/5AcknnMfBw +## MSRV -### MSRV -The Minimum Supported Rust Version (MSRV) is currently 1.85.0. +The Minimum Supported Rust Version (MSRV) is 1.85.0. diff --git a/impls/Cargo.toml b/impls/Cargo.toml index 9172cd88..da00d299 100644 --- a/impls/Cargo.toml +++ b/impls/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "impls" -version = "0.1.0" +version.workspace = true edition = "2021" rust-version.workspace = true diff --git a/server/Cargo.toml b/server/Cargo.toml index 57739c04..3e03a95e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vss-server" -version = "0.1.0" +version.workspace = true edition = "2021" rust-version.workspace = true diff --git a/server/src/main.rs b/server/src/main.rs index 12277d28..70205e54 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -87,7 +87,10 @@ fn main() { }, }; + #[cfg(any(feature = "jwt", feature = "sigs"))] let mut authorizer: Option> = None; + #[cfg(not(any(feature = "jwt", feature = "sigs")))] + let authorizer: Option> = None; #[cfg(feature = "jwt")] { if let Some(rsa_pem) = config.rsa_pem { @@ -137,10 +140,7 @@ fn main() { error!("Failed to start postgres TLS backend: {}", e); std::process::exit(-1); }); - info!( - "Connected to PostgreSQL TLS backend with DSN: {}/{}", - config.postgresql_prefix, config.vss_db - ); + info!("Connected to PostgreSQL TLS backend, database {}", config.vss_db); Arc::new(postgres_tls_backend) } else { let postgres_plaintext_backend = PostgresPlaintextBackend::new( @@ -153,10 +153,7 @@ fn main() { error!("Failed to start postgres plaintext backend: {}", e); std::process::exit(-1); }); - info!( - "Connected to PostgreSQL plaintext backend with DSN: {}/{}", - config.postgresql_prefix, config.vss_db - ); + info!("Connected to PostgreSQL plaintext backend, database {}", config.vss_db); Arc::new(postgres_plaintext_backend) }; diff --git a/server/src/util/config.rs b/server/src/util/config.rs index 65603343..dbf8d8fb 100644 --- a/server/src/util/config.rs +++ b/server/src/util/config.rs @@ -6,6 +6,7 @@ const BIND_ADDR_VAR: &str = "VSS_BIND_ADDRESS"; const MAX_REQUEST_BODY_SIZE_VAR: &str = "VSS_MAX_REQUEST_BODY_SIZE"; const LOG_FILE_VAR: &str = "VSS_LOG_FILE"; const LOG_LEVEL_VAR: &str = "VSS_LOG_LEVEL"; +#[cfg(feature = "jwt")] const JWT_RSA_PEM_VAR: &str = "VSS_JWT_RSA_PEM"; const PSQL_USER_VAR: &str = "VSS_PSQL_USERNAME"; const PSQL_PASS_VAR: &str = "VSS_PSQL_PASSWORD"; @@ -21,6 +22,7 @@ const PSQL_CERT_PEM_VAR: &str = "VSS_PSQL_CRT_PEM"; struct TomlConfig { server_config: Option, log_config: Option, + #[cfg(feature = "jwt")] jwt_auth_config: Option, postgresql_config: Option, } @@ -31,6 +33,7 @@ struct ServerConfig { max_request_body_size: Option, } +#[cfg(feature = "jwt")] #[derive(Deserialize)] struct JwtAuthConfig { rsa_pem: Option, @@ -61,6 +64,7 @@ struct LogConfig { pub(crate) struct Configuration { pub(crate) bind_address: String, pub(crate) max_request_body_size: Option, + #[cfg(feature = "jwt")] pub(crate) rsa_pem: Option, pub(crate) postgresql_prefix: String, pub(crate) default_db: String, @@ -90,16 +94,21 @@ fn read_config<'a, T: std::fmt::Display>( } pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result { - let TomlConfig { server_config, log_config, jwt_auth_config, postgresql_config } = - match config_file_path { - Some(path) => { - let config_file = std::fs::read_to_string(path) - .map_err(|e| format!("Failed to read configuration file: {}", e))?; - toml::from_str(&config_file) - .map_err(|e| format!("Failed to parse configuration file: {}", e))? - }, - None => TomlConfig::default(), // All fields are set to `None` - }; + let TomlConfig { + server_config, + log_config, + #[cfg(feature = "jwt")] + jwt_auth_config, + postgresql_config, + } = match config_file_path { + Some(path) => { + let config_file = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read configuration file: {}", e))?; + toml::from_str(&config_file) + .map_err(|e| format!("Failed to parse configuration file: {}", e))? + }, + None => TomlConfig::default(), // All fields are set to `None` + }; let (bind_address_config, max_request_body_size_config) = match server_config { Some(c) => (c.bind_address, c.max_request_body_size), @@ -151,8 +160,11 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result = log_config.and_then(|config| config.file); let log_file = log_file_env.or(log_file_config).unwrap_or(PathBuf::from("vss.log")); - let rsa_pem_env = read_env(JWT_RSA_PEM_VAR)?; - let rsa_pem = rsa_pem_env.or(jwt_auth_config.and_then(|config| config.rsa_pem)); + #[cfg(feature = "jwt")] + let rsa_pem = { + let rsa_pem_env = read_env(JWT_RSA_PEM_VAR)?; + rsa_pem_env.or(jwt_auth_config.and_then(|config| config.rsa_pem)) + }; let username_env = read_env(PSQL_USER_VAR)?; let password_env = read_env(PSQL_PASS_VAR)?; @@ -206,6 +218,7 @@ pub(crate) fn load_configuration(config_file_path: Option<&str>) -> Result