Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b04461c885 | |||
| 2c5919a972 | |||
| 169dcd9811 |
13 changed files with 782 additions and 33 deletions
147
Cargo.lock
generated
147
Cargo.lock
generated
|
|
@ -130,6 +130,18 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "atoi"
|
||||
version = "2.0.0"
|
||||
|
|
@ -232,6 +244,26 @@ version = "1.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
"unty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
|
|
@ -253,6 +285,19 @@ dependencies = [
|
|||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
|
@ -320,6 +365,12 @@ version = "0.9.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
|
|
@ -392,6 +443,33 @@ dependencies = [
|
|||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
|
|
@ -456,6 +534,32 @@ dependencies = [
|
|||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"serde",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
|
|
@ -578,6 +682,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
|
|
@ -1655,6 +1765,15 @@ version = "0.1.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.3"
|
||||
|
|
@ -1721,13 +1840,19 @@ dependencies = [
|
|||
"aes-gcm-siv",
|
||||
"axum",
|
||||
"base64",
|
||||
"bincode",
|
||||
"blake3",
|
||||
"dotenvy",
|
||||
"ed25519",
|
||||
"ed25519-dalek",
|
||||
"env_logger",
|
||||
"log",
|
||||
"p256",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"syn",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower",
|
||||
|
|
@ -1761,6 +1886,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
|
|
@ -2158,9 +2289,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -2428,6 +2559,12 @@ version = "0.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
|
|
@ -2469,6 +2606,12 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||
|
||||
[[package]]
|
||||
name = "vsss-rs"
|
||||
version = "5.1.0"
|
||||
|
|
|
|||
10
Cargo.toml
10
Cargo.toml
|
|
@ -9,6 +9,10 @@ default = ["shamir"]
|
|||
insecure-dev-sealing = []
|
||||
shamir = ["vsss-rs", "p256"]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
path = "src/macros.rs"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.27"
|
||||
env_logger = "0.11.7"
|
||||
|
|
@ -36,6 +40,12 @@ sqlx = { version = "0.8.3", features = [
|
|||
aes-gcm-siv = "0.11.1"
|
||||
vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] }
|
||||
p256 = { version = "0.13.2", optional = true, default-features = false, features = ["std", "ecdsa"] }
|
||||
blake3 = { version = "1.8.2" }
|
||||
bincode = { version = "2.0.1", features = ["serde"] }
|
||||
ed25519 = { version = "2.2.3", features = ["serde"] }
|
||||
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
|
||||
syn = "2.0.101"
|
||||
quote = "1.0.40"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
-- Add migration script here
|
||||
|
||||
CREATE TABLE kv2_engine_cfg (
|
||||
engine_path TEXT PRIMARY KEY REFERENCES secret_engines (mount_point),
|
||||
max_versions UNSIGNED INTEGER CHECK ( max_versions > 0 ), -- Shall be proper NULL if 0
|
||||
max_age_secs UNSIGNED INTEGER CHECK ( max_versions > 0 ), -- Shall be proper NULL if 0
|
||||
cas_required BOOLEAN NOT NULL DEFAULT (FALSE)
|
||||
);
|
||||
|
||||
CREATE TABLE kv2_metadata (
|
||||
engine_path TEXT NOT NULL,
|
||||
engine_path TEXT NOT NULL REFERENCES secret_engines (mount_point),
|
||||
secret_path TEXT NOT NULL,
|
||||
|
||||
cas_required INTEGER NOT NULL, -- no bool datatype in sqlite
|
||||
created_time TIMESTAMP NOT NULL,
|
||||
delete_version_after TEXT, -- Maybe NOT NULL
|
||||
delete_version_after TEXT, -- May be NULL
|
||||
max_versions INTEGER NOT NULL,
|
||||
-- current_version INTEGER NOT NULL,
|
||||
-- oldest_version INTEGER NOT NULL,
|
||||
updated_time TIMESTAMP NOT NULL,
|
||||
custom_data TEXT,
|
||||
|
||||
|
|
@ -27,6 +32,8 @@ CREATE TABLE kv2_secret_version (
|
|||
encrypted_data BLOB NOT NULL,
|
||||
nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ),
|
||||
|
||||
signature BLOB NOT NULL,
|
||||
|
||||
PRIMARY KEY (engine_path, secret_path, version_number),
|
||||
FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use axum::{
|
||||
Json,
|
||||
body::Body,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -29,6 +29,90 @@ where
|
|||
{
|
||||
match value {
|
||||
Some(data) => serializer.serialize_str(data),
|
||||
None => Err(serde::ser::Error::custom("`secret_data` must not be None during serialization!")),
|
||||
None => Err(serde::ser::Error::custom(
|
||||
"`secret_data` must not be None during serialization!",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses duration strings to seconds.
|
||||
/// Returns `None` on error or empty string.
|
||||
/// Example: `4h3m1s`
|
||||
pub fn parse_duration_str(input: &String) -> Option<u32> {
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
input
|
||||
.split_inclusive(char::is_alphabetic)
|
||||
.try_fold(0u32, |acc, chunk| {
|
||||
let (value, unit) = chunk.split_at(chunk.len() - 1);
|
||||
let value = value.parse::<u32>().ok()?;
|
||||
match unit {
|
||||
"h" => acc.checked_add(value.checked_mul(3600)?),
|
||||
"m" => acc.checked_add(value.checked_mul(60)?),
|
||||
"s" => acc.checked_add(value),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_str_valid_inputs() {
|
||||
let cases = vec![
|
||||
("0s", 0),
|
||||
("4h", 14400),
|
||||
("3m", 180),
|
||||
("1s", 1),
|
||||
("4h3m1s", 14581),
|
||||
("2h30m", 9000),
|
||||
("2h30s", 7230),
|
||||
("2m30s", 150),
|
||||
];
|
||||
|
||||
for (str, res) in cases {
|
||||
assert_eq!(parse_duration_str(&str.to_string()), Some(res))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_str_invalid_inputs() {
|
||||
let cases = vec!["", "-5s", "4x", "4h3x1s", "4h-3m1s", "4h3m1"];
|
||||
|
||||
for str in cases {
|
||||
assert_eq!(parse_duration_str(&str.to_string()), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_str_edge_cases() {
|
||||
let cases = vec![
|
||||
("0h0m0s", Some(0)),
|
||||
("0h", Some(0)),
|
||||
("0m", Some(0)),
|
||||
("0s", Some(0)),
|
||||
("1h0m0s", Some(3600)),
|
||||
("1m1000000s", Some(60 + 1000000)),
|
||||
];
|
||||
|
||||
for (str, res) in cases {
|
||||
assert_eq!(parse_duration_str(&str.to_string()), res)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_str_overflow() {
|
||||
let cases = vec![
|
||||
"100000000h".to_string(), //
|
||||
"100000000m".to_string(), //
|
||||
format!("{}s", u32::MAX as u64 + 1),
|
||||
];
|
||||
|
||||
for str in cases {
|
||||
assert_eq!(parse_duration_str(&str), None, "Failed for {str}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
mod structs;
|
||||
mod data;
|
||||
mod meta;
|
||||
mod structs;
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests;
|
||||
|
||||
use crate::storage::DbPool;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::*,
|
||||
};
|
||||
use crate::{common::parse_duration_str, storage::DbPool};
|
||||
use axum::{Extension, Json, Router, extract::State, response::NoContent, routing::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::EnginePath;
|
||||
|
||||
pub fn kv_router(pool: DbPool) -> Router {
|
||||
Router::new()
|
||||
|
|
@ -28,7 +28,7 @@ pub fn kv_router(pool: DbPool) -> Router {
|
|||
.route("/metadata/{*path}", post(meta::post_meta))
|
||||
.route("/metadata/{*path}", delete(meta::delete_meta))
|
||||
.route("/subkeys/{*path}", get(get_subkeys))
|
||||
.route("/undelete/{*path}", post(post_undelete))
|
||||
// .route("/undelete/{*path}", post(data::post_undelete)) // TODO
|
||||
.with_state(pool)
|
||||
}
|
||||
|
||||
|
|
@ -36,14 +36,51 @@ async fn get_config() -> &'static str {
|
|||
todo!("not implemented")
|
||||
}
|
||||
|
||||
async fn post_config() -> &'static str {
|
||||
todo!("not implemented")
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Config {
|
||||
/// A value `0` shall be be treated as 10
|
||||
/// TODO: Not implemented
|
||||
pub max_versions: Option<u32>,
|
||||
#[serde(default)]
|
||||
// TODO: Not implemented
|
||||
pub cas_required: bool,
|
||||
/// Max age of a secret version
|
||||
/// Example: `"3h25m19s"`
|
||||
/// `0s` disable automatic deletion
|
||||
/// TODO: Not implemented
|
||||
pub delete_version_after: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct ConfigRes {
|
||||
pub data: Config,
|
||||
}
|
||||
|
||||
async fn post_config(
|
||||
State(pool): State<DbPool>,
|
||||
Extension(EnginePath(engine_path)): Extension<EnginePath>,
|
||||
Json(config): Json<Config>,
|
||||
) -> NoContent {
|
||||
let max_age_secs: Option<u32> = config
|
||||
.delete_version_after
|
||||
.map(|v| parse_duration_str(&v).expect("Failed to parse duration string"));
|
||||
|
||||
// TODO: This
|
||||
let a = sqlx::query!(
|
||||
"UPDATE kv2_engine_cfg SET (max_versions, max_age_secs, cas_required) = ($2, $3, $4)
|
||||
WHERE engine_path = $1",
|
||||
engine_path,
|
||||
config.max_versions,
|
||||
max_age_secs,
|
||||
config.cas_required
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
NoContent
|
||||
}
|
||||
|
||||
async fn get_subkeys() -> &'static str {
|
||||
todo!("not implemented")
|
||||
}
|
||||
|
||||
async fn post_undelete() -> &'static str {
|
||||
todo!("not implemented")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::structs::KvV2WriteRequest;
|
|||
use crate::{
|
||||
common::HttpError, engines::{
|
||||
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath
|
||||
}, storage::sealing::Secret, DbPool
|
||||
}, signing::Verifiable, storage::{sealing::Secret, SecretDataDTO}, DbPool
|
||||
};
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
|
|
@ -11,7 +11,7 @@ use axum::{
|
|||
response::{IntoResponse, NoContent, Response},
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::{OffsetDateTime, UtcDateTime};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -23,6 +23,9 @@ pub struct GetDataQuery {
|
|||
}
|
||||
|
||||
/// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations
|
||||
#[rvault_server::signed_dbo]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[deprecated("Use DTO instead")]
|
||||
struct SecretDataInternal {
|
||||
pub created_time: OffsetDateTime,
|
||||
pub deletion_time: Option<OffsetDateTime>,
|
||||
|
|
@ -33,7 +36,7 @@ struct SecretDataInternal {
|
|||
pub encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SecretDataInternal {
|
||||
impl SecretDataDTO {
|
||||
pub async fn into_external(self) -> KvSecretData {
|
||||
let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await;
|
||||
KvSecretData {
|
||||
|
|
@ -57,16 +60,16 @@ pub async fn get_data(
|
|||
let res = if params.version != 0 {
|
||||
// With specific version
|
||||
sqlx::query_as!(
|
||||
SecretDataInternal,
|
||||
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path
|
||||
SecretDataDTO,
|
||||
r#"SELECT *
|
||||
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
|
||||
AND version_number = $3"#,
|
||||
engine_path, path, params.version).fetch_one(&pool).await
|
||||
} else {
|
||||
// Without specific version
|
||||
sqlx::query_as!(
|
||||
SecretDataInternal,
|
||||
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path
|
||||
SecretDataDTO,
|
||||
r#"SELECT *
|
||||
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
|
||||
ORDER BY version_number DESC LIMIT 1"#,
|
||||
engine_path, path).fetch_one(&pool).await
|
||||
|
|
@ -132,6 +135,17 @@ pub async fn post_data(
|
|||
ON CONFLICT(engine_path, secret_path) DO NOTHING;
|
||||
", engine_path, kv_path, ts).execute(&mut *tx).await.unwrap();
|
||||
|
||||
let secret_version = SecretDataDTO {
|
||||
signature: Vec::new(),
|
||||
created_time: todo!(),
|
||||
deletion_time: todo!(),
|
||||
version_number: todo!(),
|
||||
secret_path: todo!(),
|
||||
engine_path,
|
||||
nonce: todo!(),
|
||||
encrypted_data: todo!(),
|
||||
};
|
||||
let signature = secret_version.sign().await.to_vec();
|
||||
let res_r = sqlx::query_file!(
|
||||
"src/engines/kv/post_secret.sql",
|
||||
engine_path,
|
||||
|
|
@ -140,6 +154,7 @@ pub async fn post_data(
|
|||
protected_data,
|
||||
ts,
|
||||
secret.version,
|
||||
signature,
|
||||
)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
|
|
@ -229,6 +244,42 @@ pub async fn delete_data(
|
|||
Ok(NoContent.into_response())
|
||||
}
|
||||
|
||||
pub struct UndeleteReq {
|
||||
versions: Vec<i64>,
|
||||
}
|
||||
|
||||
pub async fn post_undelete(
|
||||
State(pool): State<DbPool>,
|
||||
Path(path): Path<String>,
|
||||
Extension(EnginePath(engine_path)): Extension<EnginePath>,
|
||||
Json(UndeleteReq { versions }): Json<UndeleteReq>,
|
||||
) -> Response {
|
||||
info!("Undeleting versions {versions:?} of {path} from {engine_path}");
|
||||
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
for version in versions {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE kv2_secret_version
|
||||
SET deletion_time = NULL
|
||||
WHERE engine_path = $1 AND secret_path = $2
|
||||
AND version_number = $3
|
||||
"#,
|
||||
engine_path,
|
||||
path,
|
||||
version,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
}
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
NoContent.into_response()
|
||||
}
|
||||
|
||||
|
||||
pub async fn patch_data(
|
||||
State(pool): State<DbPool>,
|
||||
Path(kv_path): Path<String>,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
WITH latest_version AS (
|
||||
SELECT MAX(version_number) AS max_version
|
||||
SELECT MAX(version_number) AS max_version, signature
|
||||
FROM kv2_secret_version
|
||||
WHERE engine_path = $1 AND secret_path = $2 -- engine_path AND secret_path
|
||||
)
|
||||
INSERT INTO kv2_secret_version (engine_path, secret_path, nonce, encrypted_data, created_time, version_number)
|
||||
INSERT INTO kv2_secret_version (engine_path, secret_path, nonce, encrypted_data, created_time, version_number, signature)
|
||||
VALUES (
|
||||
$1, -- engine_path
|
||||
$2, -- secret_path
|
||||
|
|
@ -14,6 +14,7 @@ VALUES (
|
|||
CASE -- Use provided version if given
|
||||
WHEN $6 IS NOT NULL THEN $6 -- version_number (optional)
|
||||
ELSE COALESCE((SELECT max_version FROM latest_version) + 1, 1) -- otherwise 1
|
||||
END -- version_number logic
|
||||
END, -- version_number logic
|
||||
$7 -- signature
|
||||
)
|
||||
RETURNING version_number;
|
||||
RETURNING version_number, signature;
|
||||
|
|
|
|||
215
src/macros.rs
Normal file
215
src/macros.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
extern crate proc_macro;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use sqlx::query;
|
||||
use syn::{parse_macro_input, token::Token, Fields, ItemStruct, LitStr};
|
||||
|
||||
/// Database Objects which are verifiable for integrity.\
|
||||
/// Extends struct with a `signature` attribute/field for the signature of the hash,
|
||||
/// which is skipped on serialization/deserialization.
|
||||
///
|
||||
/// After obtaining a verifiable struct, you may want to verify.
|
||||
/// After modifying, you may want to re-sign the data before updating the database entry,
|
||||
/// otherwise the saved data would violate integrity.
|
||||
///
|
||||
/// Implements the [crate::storage::signing::Verifiable] trait for usage.\
|
||||
/// Implies [serde::Serialize] due to hashing.\
|
||||
/// Only named structs are supported.
|
||||
#[proc_macro_attribute]
|
||||
pub fn signed_dbo(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as ItemStruct);
|
||||
|
||||
let vis = &input.vis;
|
||||
let struct_name = &input.ident;
|
||||
let fields = match &input.fields {
|
||||
Fields::Named(f) => &f.named,
|
||||
_ => panic!("Only named structs are supported"),
|
||||
};
|
||||
|
||||
let mut new_fields = quote! {
|
||||
#[serde(skip)]
|
||||
pub signature: Vec<u8>,
|
||||
};
|
||||
for field in fields {
|
||||
new_fields.extend(quote! { #field, });
|
||||
}
|
||||
// expand_input
|
||||
// let a = sqlx::query(r"SELECT name
|
||||
// FROM pragma_table_info('kv2_metadata')
|
||||
// WHERE pk > 0").fetch_one(&input.);
|
||||
// print!("aaa {a}");
|
||||
|
||||
let expanded = quote! {
|
||||
#[derive(serde::Serialize)]
|
||||
#vis struct #struct_name {
|
||||
#new_fields
|
||||
}
|
||||
|
||||
impl crate::storage::signing::Verifiable for #struct_name {
|
||||
async fn sign(&self) -> ed25519::Signature {
|
||||
crate::storage::signing::sign(self).await
|
||||
}
|
||||
|
||||
async fn verify<P: serde::Serialize>(
|
||||
&self,
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<(), ed25519::Error> {
|
||||
crate::storage::signing::verify(self, signature).await
|
||||
}
|
||||
}
|
||||
|
||||
impl #struct_name {
|
||||
async fn fetch_one() {
|
||||
// sqlx::query_as!(#struct_name, r"
|
||||
// SELECT * FROM $1 WHERE engine_path = $2, path = $3
|
||||
// ")
|
||||
}
|
||||
|
||||
async fn insert_one(&mut self) {
|
||||
// self.signature = await self.sign();
|
||||
// sqlx::query!(r"
|
||||
// INSERT INTO $1
|
||||
// ")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
// #[proc_macro]
|
||||
// pub fn verifying_query(input: TokenStream) -> TokenStream {
|
||||
// struct VerifyingQueryInput {
|
||||
// target_type: syn::Type,
|
||||
// table_name: syn::LitStr,
|
||||
// }
|
||||
|
||||
// impl syn::parse::Parse for VerifyingQueryInput {
|
||||
// fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
// let target_type: syn::Type = input.parse()?;
|
||||
// input.parse::<syn::Token![,]>()?;
|
||||
// let table_name: syn::LitStr = input.parse()?;
|
||||
// Ok(VerifyingQueryInput { target_type, table_name })
|
||||
// }
|
||||
// }
|
||||
|
||||
// let VerifyingQueryInput { target_type, table_name } = parse_macro_input!(input as VerifyingQueryInput);
|
||||
|
||||
// // Extract the type (e.g., `MyType`), separated by a comma
|
||||
// let parsed_input: VerifyingQueryInput = syn::parse(input).expect("Failed to parse input");
|
||||
// let target_type = parsed_input.target_type;
|
||||
// let table = parsed_input.table_name.value();
|
||||
|
||||
// let sql = format!("SELECT * FROM {table}");
|
||||
// let query = quote! {
|
||||
// sqlx::query_as!(target_type, sql)
|
||||
// };
|
||||
|
||||
// query.into()
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
#[deprecated(note = "doesnt work")]
|
||||
mod test_macro {
|
||||
use super::*;
|
||||
use quote::quote;
|
||||
use syn::{ItemStruct, parse_quote};
|
||||
|
||||
pub struct TestStruct {
|
||||
field1: String,
|
||||
field2: i32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signed_dbo_macro() {
|
||||
let input: TokenStream = quote! {}.into();
|
||||
|
||||
let output = signed_dbo(TokenStream::new(), input);
|
||||
|
||||
let expected: TokenStream = quote! {
|
||||
pub struct TestStruct {
|
||||
signature: Vec<u8>,
|
||||
field1: String,
|
||||
field2: i32,
|
||||
}
|
||||
}
|
||||
.into();
|
||||
|
||||
assert_eq!(output.to_string(), expected.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Only named structs are supported")]
|
||||
fn test_signed_dbo_macro_unnamed_struct() {
|
||||
let input: TokenStream = quote! {
|
||||
struct TestStruct(String, i32);
|
||||
}
|
||||
.into();
|
||||
|
||||
signed_dbo(TokenStream::new(), input);
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_sqlx_select_macro() {
|
||||
// let input: TokenStream = quote! {
|
||||
// MyModel, "users", "WHERE id = ?", 42
|
||||
// }
|
||||
// .into();
|
||||
|
||||
// let output = sqlx_select(input);
|
||||
|
||||
// let expected: TokenStream = quote! {
|
||||
// sqlx::query_as!(
|
||||
// MyModel,
|
||||
// "SELECT id,name,email FROM users WHERE id = ?",
|
||||
// 42
|
||||
// )
|
||||
// }
|
||||
// .into();
|
||||
|
||||
// assert_eq!(output.to_string(), expected.to_string());
|
||||
// }
|
||||
}
|
||||
|
||||
// struct SelectInput {
|
||||
// model: proc_macro::Ident,
|
||||
// _comma1: syn::Token![,],
|
||||
// table: syn::LitStr,
|
||||
// _comma2: syn::Token![,],
|
||||
// condition: syn::LitStr,
|
||||
// _comma3: syn::Token![,],
|
||||
// args: syn::punctuated::Punctuated<syn::Expr, syn::Token![,]>,
|
||||
// }
|
||||
|
||||
// #[proc_macro]
|
||||
// pub fn sqlx_select(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
// let SelectInput { model, table, condition, args, .. } = syn::parse_macro_input!(input as SelectInput);
|
||||
|
||||
// // Hardcoded columns - this would be read from metadata in a full implementation
|
||||
// let columns = quote::quote! { id, name, email };
|
||||
|
||||
// let sql = format!(
|
||||
// "SELECT {} FROM {} {}",
|
||||
// columns.to_string().replace(' ', ""),
|
||||
// table.value(),
|
||||
// condition.value()
|
||||
// );
|
||||
|
||||
// let genn = quote::quote! {
|
||||
// sqlx::query_as!(
|
||||
// #model,
|
||||
// #sql,
|
||||
// #args
|
||||
// )
|
||||
// };
|
||||
|
||||
// genn.into()
|
||||
// }
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test_macro {
|
||||
// #[test]
|
||||
// fn test_aaaah() {
|
||||
// select_all!("aaa", "bbb");
|
||||
// }
|
||||
// }
|
||||
|
|
@ -22,6 +22,8 @@ mod identity;
|
|||
mod storage;
|
||||
mod sys;
|
||||
|
||||
pub use storage::signing;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
pub mod sealing;
|
||||
pub mod signing;
|
||||
mod dtos;
|
||||
|
||||
pub use dtos::*;
|
||||
|
||||
use std::{fs::File, path::Path};
|
||||
|
||||
|
|
|
|||
14
src/storage/dtos.rs
Normal file
14
src/storage/dtos.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use time::OffsetDateTime;
|
||||
|
||||
/// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations
|
||||
#[rvault_server::signed_dbo]
|
||||
pub struct SecretDataDTO {
|
||||
pub created_time: OffsetDateTime,
|
||||
pub deletion_time: Option<OffsetDateTime>,
|
||||
pub version_number: i64,
|
||||
pub secret_path: String,
|
||||
pub engine_path: String,
|
||||
|
||||
pub nonce: Vec<u8>,
|
||||
pub encrypted_data: Vec<u8>,
|
||||
}
|
||||
|
|
@ -145,7 +145,7 @@ fn share_keys(
|
|||
limit: usize,
|
||||
root_key: &[u8],
|
||||
) -> Vec<String> {
|
||||
log::debug!("RK: {root_key:?}");
|
||||
// log::debug!("RK: {root_key:?}");
|
||||
assert!(
|
||||
threshold <= limit,
|
||||
"Threshold cannot be higher than the number of shares (limit)"
|
||||
|
|
|
|||
181
src/storage/signing.rs
Normal file
181
src/storage/signing.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use bincode::{config, serde::encode_to_vec};
|
||||
use ed25519::signature::{Signer, Verifier};
|
||||
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::RwLock;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub type SignatureBundle =
|
||||
SignatureKeyBundle<ed25519_dalek::SigningKey, ed25519_dalek::VerifyingKey>;
|
||||
|
||||
static SIGNATURE_BUNDLE: LazyLock<RwLock<SignatureBundle>> =
|
||||
LazyLock::new(|| RwLock::new(SignatureKeyBundle::new()));
|
||||
|
||||
pub trait Verifiable {
|
||||
fn sign(&self) -> impl std::future::Future<Output = ed25519::Signature> + Send;
|
||||
fn verify<P: Serialize>(
|
||||
&self,
|
||||
signature: &ed25519::Signature,
|
||||
) -> impl std::future::Future<Output = Result<(), ed25519::Error>> + Send;
|
||||
}
|
||||
|
||||
#[derive(Zeroize)]
|
||||
struct SignatureKeyBundle<S, V>
|
||||
where
|
||||
S: Signer<ed25519::Signature>,
|
||||
V: Verifier<ed25519::Signature>,
|
||||
{
|
||||
pub signing_key: S,
|
||||
pub verifying_key: V,
|
||||
}
|
||||
|
||||
impl<S, V> SignatureKeyBundle<S, V>
|
||||
where
|
||||
S: Signer<ed25519::Signature>,
|
||||
V: Verifier<ed25519::Signature>,
|
||||
{
|
||||
/// Signs a serializable payload
|
||||
pub fn sign<P: Serialize>(&self, payload: &P) -> ed25519::Signature {
|
||||
self.sign_bytes(&SignatureKeyBundle::<S, V>::hash_struct(payload))
|
||||
}
|
||||
|
||||
fn sign_bytes(&self, payload: &[u8]) -> ed25519::Signature {
|
||||
// NOTE: use `try_sign` if you'd like to be able to handle
|
||||
// errors from external signing services/devices (e.g. HSM/KMS)
|
||||
// <https://docs.rs/signature/latest/signature/trait.Signer.html#tymethod.try_sign>
|
||||
self.signing_key.sign(payload)
|
||||
}
|
||||
|
||||
/// Verifies a serializable payload against a given signature.
|
||||
pub fn verify<P: Serialize>(
|
||||
&self,
|
||||
payload: &P,
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<(), ed25519::Error> {
|
||||
self.verify_bytes(&SignatureKeyBundle::<S, V>::hash_struct(payload), signature)
|
||||
}
|
||||
|
||||
fn verify_bytes(
|
||||
&self,
|
||||
payload: &[u8],
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<(), ed25519::Error> {
|
||||
self.verifying_key.verify(payload, signature)
|
||||
}
|
||||
|
||||
/// Serializes and hashes payload.
|
||||
/// Uses `bincode` for serialization and `blake3` for hashing.
|
||||
fn hash_struct<P: Serialize>(payload: &P) -> [u8; blake3::OUT_LEN] {
|
||||
let serialized_payload =
|
||||
encode_to_vec(payload, config::standard()).expect("Failed to serialize payload");
|
||||
let hash: blake3::Hash = blake3::hash(&serialized_payload);
|
||||
let hash_bytes = hash.as_bytes();
|
||||
|
||||
*hash_bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureKeyBundle<SigningKey, VerifyingKey> {
|
||||
pub fn new() -> SignatureKeyBundle<SigningKey, VerifyingKey> {
|
||||
let mut rng = aes_gcm_siv::aead::OsRng;
|
||||
let signing_key = SigningKey::generate(&mut rng);
|
||||
let verifying_key: VerifyingKey = signing_key.verifying_key();
|
||||
Self {
|
||||
signing_key,
|
||||
verifying_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sign<P: Serialize>(payload: &P) -> ed25519::Signature {
|
||||
SIGNATURE_BUNDLE.read().await.sign(payload)
|
||||
}
|
||||
|
||||
pub async fn verify<P: Serialize>(
|
||||
payload: &P,
|
||||
signature: &ed25519::Signature,
|
||||
) -> Result<(), ed25519::Error> {
|
||||
SIGNATURE_BUNDLE.read().await.verify(payload, signature)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TestPayload {
|
||||
data: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TestPayloadEvolution {
|
||||
data: String,
|
||||
new_prop: Option<bool>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_and_verify() {
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
let payload = TestPayload {
|
||||
data: "test data".to_string(),
|
||||
};
|
||||
|
||||
let signature = key_bundle.sign(&payload);
|
||||
assert!(key_bundle.verify(&payload, &signature).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_with_invalid_signature() {
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
let payload = TestPayload {
|
||||
data: "test data".to_string(),
|
||||
};
|
||||
|
||||
let signature = key_bundle.sign(&payload);
|
||||
|
||||
let invalid_payload = TestPayload {
|
||||
data: "tampered data".to_string(),
|
||||
};
|
||||
|
||||
assert!(key_bundle.verify(&invalid_payload, &signature).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_with_different_bundles() {
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
let payload = TestPayload {
|
||||
data: "test data".to_string(),
|
||||
};
|
||||
let signature = key_bundle.sign(&payload);
|
||||
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
assert!(key_bundle.verify(&payload, &signature).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_bytes_and_verify_bytes() {
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
let payload = b"test bytes";
|
||||
|
||||
let signature = key_bundle.sign_bytes(payload);
|
||||
assert!(key_bundle.verify_bytes(payload, &signature).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_bytes_with_invalid_signature() {
|
||||
let key_bundle = SignatureKeyBundle::new();
|
||||
let payload = b"test bytes";
|
||||
|
||||
let signature = key_bundle.sign_bytes(payload);
|
||||
|
||||
let invalid_payload = b"tampered bytes";
|
||||
|
||||
assert!(
|
||||
key_bundle
|
||||
.verify_bytes(invalid_payload, &signature)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue