This commit is contained in:
Laurenz 2025-05-13 14:14:06 +02:00
parent 169dcd9811
commit 2c5919a972
Signed by: C0ffeeCode
SSH key fingerprint: SHA256:prvFOyBjButRypyXm7X8lbbCkly2Dq1PF7e/mrsPVjw
10 changed files with 585 additions and 10 deletions

147
Cargo.lock generated
View file

@ -130,6 +130,18 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -232,6 +244,26 @@ version = "1.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.0" version = "2.9.0"
@ -253,6 +285,19 @@ dependencies = [
"wyz", "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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -320,6 +365,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.17" version = "0.2.17"
@ -392,6 +443,33 @@ dependencies = [
"cipher", "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]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -456,6 +534,32 @@ dependencies = [
"spki", "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]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@ -578,6 +682,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.1" version = "0.11.1"
@ -1655,6 +1765,15 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 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]] [[package]]
name = "rustix" name = "rustix"
version = "1.0.3" version = "1.0.3"
@ -1721,13 +1840,19 @@ dependencies = [
"aes-gcm-siv", "aes-gcm-siv",
"axum", "axum",
"base64", "base64",
"bincode",
"blake3",
"dotenvy", "dotenvy",
"ed25519",
"ed25519-dalek",
"env_logger", "env_logger",
"log", "log",
"p256", "p256",
"quote",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"syn",
"time", "time",
"tokio", "tokio",
"tower", "tower",
@ -1761,6 +1886,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@ -2158,9 +2289,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.100" version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2428,6 +2559,12 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unty"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@ -2469,6 +2606,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "virtue"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
[[package]] [[package]]
name = "vsss-rs" name = "vsss-rs"
version = "5.1.0" version = "5.1.0"

View file

@ -9,6 +9,10 @@ default = ["shamir"]
insecure-dev-sealing = [] insecure-dev-sealing = []
shamir = ["vsss-rs", "p256"] shamir = ["vsss-rs", "p256"]
[lib]
proc-macro = true
path = "src/macros.rs"
[dependencies] [dependencies]
log = "0.4.27" log = "0.4.27"
env_logger = "0.11.7" env_logger = "0.11.7"
@ -36,6 +40,12 @@ sqlx = { version = "0.8.3", features = [
aes-gcm-siv = "0.11.1" aes-gcm-siv = "0.11.1"
vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] } 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"] } 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] [lints]
workspace = true workspace = true

View file

@ -32,6 +32,8 @@ CREATE TABLE kv2_secret_version (
encrypted_data BLOB NOT NULL, encrypted_data BLOB NOT NULL,
nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ), nonce BLOB NOT NULL CHECK ( length(nonce) = 12 ),
signature BLOB NOT NULL,
PRIMARY KEY (engine_path, secret_path, version_number), PRIMARY KEY (engine_path, secret_path, version_number),
FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path) FOREIGN KEY (engine_path, secret_path) REFERENCES kv2_metadata(engine_path, secret_path)
); );

View file

@ -2,7 +2,7 @@ use super::structs::KvV2WriteRequest;
use crate::{ use crate::{
common::HttpError, engines::{ common::HttpError, engines::{
kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath kv::structs::{KvSecretData, KvSecretRes, KvV2WriteResponse, Wrapper}, EnginePath
}, storage::sealing::Secret, DbPool }, signing::Verifiable, storage::{sealing::Secret, SecretDataDTO}, DbPool
}; };
use axum::{ use axum::{
Extension, Json, Extension, Json,
@ -11,7 +11,7 @@ use axum::{
response::{IntoResponse, NoContent, Response}, response::{IntoResponse, NoContent, Response},
}; };
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use time::{OffsetDateTime, UtcDateTime}; use time::{OffsetDateTime, UtcDateTime};
#[derive(Deserialize)] #[derive(Deserialize)]
@ -23,6 +23,9 @@ pub struct GetDataQuery {
} }
/// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations /// Unluckily needed as `sqlx::query_as!()` does not support FromRow derivations
#[rvault_server::signed_dbo]
#[derive(Serialize, Deserialize)]
#[deprecated("Use DTO instead")]
struct SecretDataInternal { struct SecretDataInternal {
pub created_time: OffsetDateTime, pub created_time: OffsetDateTime,
pub deletion_time: Option<OffsetDateTime>, pub deletion_time: Option<OffsetDateTime>,
@ -33,7 +36,7 @@ struct SecretDataInternal {
pub encrypted_data: Vec<u8>, pub encrypted_data: Vec<u8>,
} }
impl SecretDataInternal { impl SecretDataDTO {
pub async fn into_external(self) -> KvSecretData { pub async fn into_external(self) -> KvSecretData {
let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await; let secret = Secret::new(self.encrypted_data, self.nonce).decrypt().await;
KvSecretData { KvSecretData {
@ -57,16 +60,16 @@ pub async fn get_data(
let res = if params.version != 0 { let res = if params.version != 0 {
// With specific version // With specific version
sqlx::query_as!( sqlx::query_as!(
SecretDataInternal, SecretDataDTO,
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path r#"SELECT *
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
AND version_number = $3"#, AND version_number = $3"#,
engine_path, path, params.version).fetch_one(&pool).await engine_path, path, params.version).fetch_one(&pool).await
} else { } else {
// Without specific version // Without specific version
sqlx::query_as!( sqlx::query_as!(
SecretDataInternal, SecretDataDTO,
r#"SELECT nonce, encrypted_data, created_time, deletion_time, version_number, secret_path r#"SELECT *
FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL FROM kv2_secret_version WHERE engine_path = $1 AND secret_path = $2 AND deletion_time IS NULL
ORDER BY version_number DESC LIMIT 1"#, ORDER BY version_number DESC LIMIT 1"#,
engine_path, path).fetch_one(&pool).await engine_path, path).fetch_one(&pool).await
@ -132,6 +135,17 @@ pub async fn post_data(
ON CONFLICT(engine_path, secret_path) DO NOTHING; ON CONFLICT(engine_path, secret_path) DO NOTHING;
", engine_path, kv_path, ts).execute(&mut *tx).await.unwrap(); ", 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_content.sign().await;
let res_r = sqlx::query_file!( let res_r = sqlx::query_file!(
"src/engines/kv/post_secret.sql", "src/engines/kv/post_secret.sql",
engine_path, engine_path,
@ -140,6 +154,7 @@ pub async fn post_data(
protected_data, protected_data,
ts, ts,
secret.version, secret.version,
signature,
) )
.fetch_one(&mut *tx) .fetch_one(&mut *tx)
.await .await

204
src/macros.rs Normal file
View file

@ -0,0 +1,204 @@
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, });
}
let a = sqlx::query_unchecked!(r"SELECT name
FROM pragma_table_info('kv2_metadata')
WHERE pk > 0");
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()
}
};
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");
// }
// }

View file

@ -22,6 +22,8 @@ mod identity;
mod storage; mod storage;
mod sys; mod sys;
pub use storage::signing;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let _ = dotenvy::dotenv(); let _ = dotenvy::dotenv();

View file

@ -1,4 +1,8 @@
pub mod sealing; pub mod sealing;
pub mod signing;
mod dtos;
pub use dtos::*;
use std::{fs::File, path::Path}; use std::{fs::File, path::Path};

14
src/storage/dtos.rs Normal file
View 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>,
}

View file

@ -145,7 +145,7 @@ fn share_keys(
limit: usize, limit: usize,
root_key: &[u8], root_key: &[u8],
) -> Vec<String> { ) -> Vec<String> {
log::debug!("RK: {root_key:?}"); // log::debug!("RK: {root_key:?}");
assert!( assert!(
threshold <= limit, threshold <= limit,
"Threshold cannot be higher than the number of shares (limit)" "Threshold cannot be higher than the number of shares (limit)"

181
src/storage/signing.rs Normal file
View 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()
);
}
}