diff --git a/Cargo.lock b/Cargo.lock index c23661e..0cff435 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 107a780..3a684b3 100644 --- a/Cargo.toml +++ b/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 diff --git a/migrations/20240501152243_KvSecret.sql b/migrations/20240501152243_KvSecret.sql index c01e5a4..b3cbe13 100644 --- a/migrations/20240501152243_KvSecret.sql +++ b/migrations/20240501152243_KvSecret.sql @@ -32,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) ); diff --git a/src/engines/kv/data.rs b/src/engines/kv/data.rs index 5d23b9f..c9da4c4 100644 --- a/src/engines/kv/data.rs +++ b/src/engines/kv/data.rs @@ -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, @@ -33,7 +36,7 @@ struct SecretDataInternal { pub encrypted_data: Vec, } -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_content.sign().await; 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 diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..80844a1 --- /dev/null +++ b/src/macros.rs @@ -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, + }; + 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( + &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 { +// let target_type: syn::Type = input.parse()?; +// input.parse::()?; +// 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, + 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, +// } + +// #[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"); +// } +// } diff --git a/src/main.rs b/src/main.rs index 5cf8d2f..1a6f9e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,8 @@ mod identity; mod storage; mod sys; +pub use storage::signing; + #[tokio::main] async fn main() { let _ = dotenvy::dotenv(); diff --git a/src/storage.rs b/src/storage.rs index 97cbb25..f4f386b 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,4 +1,8 @@ pub mod sealing; +pub mod signing; +mod dtos; + +pub use dtos::*; use std::{fs::File, path::Path}; diff --git a/src/storage/dtos.rs b/src/storage/dtos.rs new file mode 100644 index 0000000..cf055f7 --- /dev/null +++ b/src/storage/dtos.rs @@ -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, + pub version_number: i64, + pub secret_path: String, + pub engine_path: String, + + pub nonce: Vec, + pub encrypted_data: Vec, +} diff --git a/src/storage/sealing/shamir.rs b/src/storage/sealing/shamir.rs index 2a9e513..b5b97f4 100644 --- a/src/storage/sealing/shamir.rs +++ b/src/storage/sealing/shamir.rs @@ -145,7 +145,7 @@ fn share_keys( limit: usize, root_key: &[u8], ) -> Vec { - log::debug!("RK: {root_key:?}"); + // log::debug!("RK: {root_key:?}"); assert!( threshold <= limit, "Threshold cannot be higher than the number of shares (limit)" diff --git a/src/storage/signing.rs b/src/storage/signing.rs new file mode 100644 index 0000000..d467021 --- /dev/null +++ b/src/storage/signing.rs @@ -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; + +static SIGNATURE_BUNDLE: LazyLock> = + LazyLock::new(|| RwLock::new(SignatureKeyBundle::new())); + +pub trait Verifiable { + fn sign(&self) -> impl std::future::Future + Send; + fn verify( + &self, + signature: &ed25519::Signature, + ) -> impl std::future::Future> + Send; +} + +#[derive(Zeroize)] +struct SignatureKeyBundle +where + S: Signer, + V: Verifier, +{ + pub signing_key: S, + pub verifying_key: V, +} + +impl SignatureKeyBundle +where + S: Signer, + V: Verifier, +{ + /// Signs a serializable payload + pub fn sign(&self, payload: &P) -> ed25519::Signature { + self.sign_bytes(&SignatureKeyBundle::::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) + // + self.signing_key.sign(payload) + } + + /// Verifies a serializable payload against a given signature. + pub fn verify( + &self, + payload: &P, + signature: &ed25519::Signature, + ) -> Result<(), ed25519::Error> { + self.verify_bytes(&SignatureKeyBundle::::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(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 { + pub fn new() -> SignatureKeyBundle { + 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(payload: &P) -> ed25519::Signature { + SIGNATURE_BUNDLE.read().await.sign(payload) +} + +pub async fn verify( + 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, + } + + #[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() + ); + } +}