added signatures
This commit is contained in:
parent
4ebb2a21d2
commit
47af1e62b7
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -7,6 +7,7 @@
|
||||
"Conn",
|
||||
"dotenv",
|
||||
"hmac",
|
||||
"minisign",
|
||||
"oneshot",
|
||||
"openapi",
|
||||
"recv",
|
||||
|
120
Cargo.lock
generated
120
Cargo.lock
generated
@ -215,10 +215,12 @@ dependencies = [
|
||||
"error-stack",
|
||||
"hmac",
|
||||
"ldap3",
|
||||
"minisign",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rust-argon2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"strum",
|
||||
@ -396,6 +398,16 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.23"
|
||||
@ -600,6 +612,12 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.9"
|
||||
@ -1264,6 +1282,15 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
@ -1464,6 +1491,18 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26541387415a1e829df5d532aad019fb11bc723e2b5bc99edefa4cf5bfad0de7"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"getrandom",
|
||||
"rpassword",
|
||||
"scrypt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.2"
|
||||
@ -1709,6 +1748,16 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.4"
|
||||
@ -1961,6 +2010,17 @@ dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.7"
|
||||
@ -1981,6 +2041,16 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-argon2"
|
||||
version = "2.1.0"
|
||||
@ -2077,6 +2147,15 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -2101,6 +2180,17 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
@ -3053,8 +3143,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.3.0"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@ -3064,8 +3155,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-axum"
|
||||
version = "0.1.3"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff0d605008ed085986e1803fd5c81d18c0f8503b1e4bbb21ea75b3fc20dd1efb"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"paste",
|
||||
@ -3076,8 +3168,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.3.0"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -3087,8 +3180,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-redoc"
|
||||
version = "5.0.0"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33749b636458b2ed3e8ebc633febffb3e4ed7298d9f749e9b71cc25ea2f0eb9f"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
@ -3098,8 +3192,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-scalar"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "088e93bf19f6bd06e0aacb02ca432b3c5a449c4aec2e4aa9fc333a667f2b2c55"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"serde",
|
||||
@ -3109,8 +3204,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-swagger-ui"
|
||||
version = "8.1.0"
|
||||
source = "git+https://github.com/juhaku/utoipa#3ffad4bed73e5caeddc311bab70f810d1d772079"
|
||||
version = "8.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "040cad8bd8de63f3d028e08e5b39be49d68f8a646e99f4aea2e2d4d82c34b21f"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
|
23
Cargo.toml
23
Cargo.toml
@ -26,6 +26,7 @@ config = "0.15.4"
|
||||
uuid = { version = "1.11.0", features = ["v4"] }
|
||||
sha2 = "0.10.8"
|
||||
hmac = "0.12.1"
|
||||
minisign = "0.7.9"
|
||||
# axum-jwt-login = { path = "../axum-login-jwt" }
|
||||
axum-jwt-login = { version = "0.1.0", registry = "kellnr" }
|
||||
rust-argon2 = "2.1.0"
|
||||
@ -38,27 +39,17 @@ windows-service = "0.7.0"
|
||||
|
||||
axum = { version = "0.8.1", features = ["macros"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
# utoipa = { version = "5.3.0", features = ["axum_extras"] }
|
||||
utoipa = { git = "https://github.com/juhaku/utoipa", features = [
|
||||
"axum_extras",
|
||||
] }
|
||||
# utoipa-axum = "0.1.3"
|
||||
utoipa-axum = { git = "https://github.com/juhaku/utoipa" }
|
||||
# utoipa-swagger-ui = { version = "8.1.0", features = ["axum"] }
|
||||
# utoipa-redoc = { version = "*", features = ["axum"] }
|
||||
# utoipa-scalar = { version = "*", features = ["axum"] }
|
||||
utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa", features = [
|
||||
"axum",
|
||||
] }
|
||||
utoipa-redoc = { git = "https://github.com/juhaku/utoipa", features = ["axum"] }
|
||||
utoipa-scalar = { git = "https://github.com/juhaku/utoipa", features = [
|
||||
"axum",
|
||||
] }
|
||||
utoipa = { version = "5.3.1", features = ["axum_extras"] }
|
||||
utoipa-axum = "0.1.4"
|
||||
utoipa-swagger-ui = { version = "8.1.1", features = ["axum"] }
|
||||
utoipa-redoc = { version = "*", features = ["axum"] }
|
||||
utoipa-scalar = { version = "*", features = ["axum"] }
|
||||
ts-rs = { version = "10.1.0", features = ["chrono-impl"] }
|
||||
|
||||
# Utilities
|
||||
# ========================================
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde_json = "1.0.134"
|
||||
tokio = { version = "1.42.0", features = ["full"] }
|
||||
tokio-util = { version = "0.7.13", features = ["rt"] }
|
||||
once_cell = "1.20.2"
|
||||
|
@ -7,6 +7,7 @@ pub const AUTH_TAG: &str = "Authentication";
|
||||
pub const USERS_TAG: &str = "Users";
|
||||
pub const ORDER_TAG: &str = "order";
|
||||
pub const API_KEY_TAG: &str = "API Keys";
|
||||
pub const SIGNATURE_TAG: &str = "Signature";
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
|
@ -1,5 +1,6 @@
|
||||
pub mod api_keys;
|
||||
pub mod auth;
|
||||
mod signature;
|
||||
pub mod users;
|
||||
|
||||
use api_keys::models::ApiKey as APIKey;
|
||||
@ -27,8 +28,8 @@ macro_rules! login_required {
|
||||
macro_rules! permission_required {
|
||||
($($perm:expr),+ $(,)?) => {
|
||||
axum_jwt_login::permission_required!(
|
||||
User,
|
||||
ApiBackend,
|
||||
crate::api::routes::User,
|
||||
crate::api::ApiBackend,
|
||||
crate::api::routes::APIKey,
|
||||
$($perm),+
|
||||
)
|
||||
@ -42,6 +43,7 @@ pub fn create_routes(session: AuthBackendType) -> Router {
|
||||
.nest(API_BASE, auth::router())
|
||||
.nest(API_BASE, users::router())
|
||||
.nest(API_BASE, api_keys::router())
|
||||
.nest(API_BASE, signature::router())
|
||||
// .nest(
|
||||
// "/api/order",
|
||||
// // order::router().route_layer(crate::login_required!(AuthenticationBackend<ApiKey>)),
|
||||
|
275
src/api/routes/signature/mod.rs
Normal file
275
src/api/routes/signature/mod.rs
Normal file
@ -0,0 +1,275 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use axum::{debug_handler, Json};
|
||||
use chrono::Utc;
|
||||
use minisign::{KeyPair, PublicKeyBox, SecretKeyBox, SignatureBox};
|
||||
use models::{
|
||||
PublicKeyID, SignatureTest, SigningKeyRequest, SigningKeyStatus, VerificationRequest,
|
||||
};
|
||||
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||
|
||||
use crate::{
|
||||
api::{
|
||||
description::SIGNATURE_TAG,
|
||||
routes::users::permissions::{Permission, PermissionDetail},
|
||||
},
|
||||
errors::ApiError,
|
||||
permission_required,
|
||||
};
|
||||
|
||||
use super::AuthBackendType;
|
||||
|
||||
pub mod models;
|
||||
pub mod sql;
|
||||
|
||||
// expose the OpenAPI to parent module
|
||||
pub fn router() -> OpenApiRouter {
|
||||
let verify = OpenApiRouter::new().routes(routes!(verify));
|
||||
let write = OpenApiRouter::new()
|
||||
.routes(routes!(
|
||||
check_signing_key,
|
||||
create_signing_key,
|
||||
verify_signing_key
|
||||
))
|
||||
.routes(routes!(sign))
|
||||
.route_layer(permission_required!(Permission::Write(
|
||||
PermissionDetail::Signature
|
||||
)));
|
||||
|
||||
OpenApiRouter::new().merge(write).merge(verify)
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/key",
|
||||
summary = "Check for signing key",
|
||||
description = "Check if the logged in user has configured a signing key.",
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn check_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?;
|
||||
|
||||
Ok(Json(SigningKeyStatus {
|
||||
configured: private_key.key.is_some(),
|
||||
verified: None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/key",
|
||||
summary = "Create signing key",
|
||||
description = "Create signing key for the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully created signing key"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn create_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// create Key Pair
|
||||
let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?;
|
||||
let public_key = pk.to_box()?.to_string();
|
||||
let private_key = sk
|
||||
.to_box(Some(&format!(
|
||||
"{} {} - {}",
|
||||
user.name,
|
||||
user.surname,
|
||||
Utc::now()
|
||||
)))? // Optional comment about the key
|
||||
.to_string();
|
||||
|
||||
// store keys in database
|
||||
sql::update_private_key(backend.pool(), &user.user_id, private_key).await?;
|
||||
sql::store_public_key(backend.pool(), &user.user_id, public_key).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/signature/key",
|
||||
summary = "Verify signing key",
|
||||
description = "Verify signing key for the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify_signing_key(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<SigningKeyStatus>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let status = match key_request
|
||||
.to_private_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
{
|
||||
Some(_key) => SigningKeyStatus {
|
||||
configured: true,
|
||||
verified: Some(true),
|
||||
},
|
||||
None => SigningKeyStatus {
|
||||
configured: false,
|
||||
verified: None,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Json(status))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/signature/sign",
|
||||
summary = "Sign something with your signing key",
|
||||
description = "Sign something as the currently logged in user.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
),
|
||||
security(
|
||||
("user_auth" = ["write:signature",]),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn sign(
|
||||
auth_session: AuthBackendType,
|
||||
Json(key_request): Json<SigningKeyRequest>,
|
||||
) -> Result<Json<VerificationRequest>, ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
let user = auth_session
|
||||
.is_authenticated()
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get private key
|
||||
let private_key = key_request
|
||||
.to_private_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
.ok_or(ApiError::AccessDenied)?;
|
||||
|
||||
// get current public key
|
||||
let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id)
|
||||
.await?
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding public key found".to_string(),
|
||||
))?;
|
||||
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let signature_box = minisign::sign(
|
||||
None,
|
||||
&private_key,
|
||||
data_reader,
|
||||
None,
|
||||
Some(&serde_json::to_string(&public_key_id)?),
|
||||
)?;
|
||||
|
||||
// We have a signature! Let's inspect it a little bit.
|
||||
println!(
|
||||
"Untrusted comment: [{}]",
|
||||
signature_box.untrusted_comment().unwrap()
|
||||
);
|
||||
println!(
|
||||
"Trusted comment: [{}]",
|
||||
signature_box.trusted_comment().unwrap()
|
||||
);
|
||||
|
||||
// store signature
|
||||
// Converting the signature box to a string in order to save it is easy.
|
||||
let signature = signature_box.into_string();
|
||||
println!("BLA");
|
||||
println!("{signature}");
|
||||
|
||||
Ok(Json(VerificationRequest {
|
||||
signature,
|
||||
signed_from: "".to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/signature/verify",
|
||||
summary = "Verify a signature",
|
||||
description = "Verify the signature.",
|
||||
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
|
||||
responses(
|
||||
(status = OK, description = "Successfully signed"),
|
||||
),
|
||||
tag = SIGNATURE_TAG)]
|
||||
pub async fn verify(
|
||||
auth_session: AuthBackendType,
|
||||
Json(verification_request): Json<VerificationRequest>,
|
||||
) -> Result<(), ApiError> {
|
||||
let backend = auth_session.backend();
|
||||
|
||||
// Now, let's verify the signature.
|
||||
// Assuming we just loaded it into `signature_box_str`, get the box back.
|
||||
let signature_box = SignatureBox::from_string(&verification_request.signature)?;
|
||||
|
||||
// get public key id from signature
|
||||
let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?;
|
||||
|
||||
// Load the public key of the user that signed.
|
||||
let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id)
|
||||
.await?
|
||||
.key
|
||||
.ok_or(ApiError::InternalError(
|
||||
"No corresponding Public Key was found.".to_string(),
|
||||
))?;
|
||||
let public_key_box = PublicKeyBox::from_string(&public_key_str)?;
|
||||
let public_key = public_key_box.into_public_key()?;
|
||||
|
||||
// And verify the data.
|
||||
let data = SignatureTest {
|
||||
value1: "blfjdljfa".to_string(),
|
||||
value2: false,
|
||||
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
|
||||
};
|
||||
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
|
||||
let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false);
|
||||
match verified {
|
||||
Ok(()) => println!("Success!"),
|
||||
Err(_) => println!("Verification failed"),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
82
src/api/routes/signature/models.rs
Normal file
82
src/api/routes/signature/models.rs
Normal file
@ -0,0 +1,82 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
};
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use minisign::{SecretKey, SecretKeyBox};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{api::routes::signature::sql, errors::ApiError};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct SigningKeyStatus {
|
||||
pub configured: bool,
|
||||
pub verified: Option<bool>,
|
||||
}
|
||||
|
||||
pub struct KeyData {
|
||||
pub key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PublicKeyID {
|
||||
pub timestamp: NaiveDateTime,
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct SigningKeyRequest {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
impl SigningKeyRequest {
|
||||
pub async fn to_private_key(
|
||||
self,
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
) -> Result<Option<SecretKey>, ApiError> {
|
||||
// try to get private key from database
|
||||
let private_key = match sql::get_private_key(pool, user_id).await?.key {
|
||||
Some(key) => key,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// create MinSign Box from key string
|
||||
let private_key_box = SecretKeyBox::from_string(&private_key)?;
|
||||
// and the box can be opened using the password to reveal the original secret key:
|
||||
let secret_key = private_key_box.into_secret_key(Some(self.secret))?;
|
||||
|
||||
Ok(Some(secret_key))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, ToSchema)]
|
||||
#[ts(export)]
|
||||
pub struct VerificationRequest {
|
||||
pub signature: String,
|
||||
pub signed_from: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct SignatureTest {
|
||||
pub value1: String,
|
||||
pub value2: bool,
|
||||
pub value3: Vec<String>,
|
||||
}
|
||||
|
||||
impl SignatureTest
|
||||
where
|
||||
Self: Hash,
|
||||
{
|
||||
pub fn calculate_hash(&self) -> u64 {
|
||||
let mut s = DefaultHasher::new();
|
||||
self.hash(&mut s);
|
||||
s.finish()
|
||||
}
|
||||
}
|
93
src/api/routes/signature/sql.rs
Normal file
93
src/api/routes/signature/sql.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::{
|
||||
api::routes::signature::models::{KeyData, PublicKeyID},
|
||||
errors::ApiError,
|
||||
};
|
||||
|
||||
pub async fn get_private_key(pool: &PgPool, user_id: &str) -> Result<KeyData, ApiError> {
|
||||
Ok(sqlx::query_as!(
|
||||
KeyData,
|
||||
r#"SELECT
|
||||
USERS."PrivateKey" as key
|
||||
FROM
|
||||
users
|
||||
WHERE USERS."UserID" = $1"#,
|
||||
user_id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn update_private_key(
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
private_key: String,
|
||||
) -> Result<(), ApiError> {
|
||||
sqlx::query!(
|
||||
r#"UPDATE users SET
|
||||
"PrivateKey" = $2
|
||||
WHERE "UserID" = $1"#,
|
||||
user_id,
|
||||
private_key
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn store_public_key(
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
public_key: String,
|
||||
) -> Result<(), ApiError> {
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO "PublicKeys"
|
||||
("UserID", "PublicKey")
|
||||
VALUES ($1, $2)"#,
|
||||
user_id,
|
||||
public_key
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_public_key(
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
) -> Result<Option<PublicKeyID>, ApiError> {
|
||||
Ok(sqlx::query_as!(
|
||||
PublicKeyID,
|
||||
r#"SELECT
|
||||
"PublicKeys"."Timestamp" as timestamp,
|
||||
"PublicKeys"."UserID" as user_id
|
||||
FROM
|
||||
"PublicKeys"
|
||||
WHERE
|
||||
"PublicKeys"."UserID" = $1
|
||||
ORDER BY timestamp DESC"#,
|
||||
user_id,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_public_key_by_id(pool: &PgPool, key_id: PublicKeyID) -> Result<KeyData, ApiError> {
|
||||
Ok(sqlx::query_as!(
|
||||
KeyData,
|
||||
r#"SELECT
|
||||
"PublicKeys"."PublicKey" as key
|
||||
FROM
|
||||
"PublicKeys"
|
||||
WHERE
|
||||
"PublicKeys"."UserID" = $1
|
||||
AND "PublicKeys"."Timestamp" = $2"#,
|
||||
key_id.user_id,
|
||||
key_id.timestamp,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await?)
|
||||
}
|
@ -61,6 +61,7 @@ impl Display for Permission {
|
||||
pub enum PermissionDetail {
|
||||
Users,
|
||||
APIKeys,
|
||||
Signature,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
use error_stack::Context;
|
||||
use minisign::PError;
|
||||
use serde::Serialize;
|
||||
use sha2::digest::InvalidLength;
|
||||
use tracing::error;
|
||||
@ -110,6 +111,18 @@ impl From<InvalidLength> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PError> for ApiError {
|
||||
fn from(value: PError) -> Self {
|
||||
Self::InternalError(format!("MiniSign Error: {value}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for ApiError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::InternalError(format!("Error on (de)serialization: {value}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ApiError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let error_info = self.as_error_info().1;
|
||||
|
@ -6,6 +6,7 @@ CREATE TABLE public.users
|
||||
"Surname" character varying(250) NOT NULL DEFAULT '',
|
||||
"Email" character varying(500) NOT NULL DEFAULT '',
|
||||
"Password" character varying(255) NOT NULL DEFAULT '',
|
||||
"PrivateKey" text COLLATE,
|
||||
"CreationDate" timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
"LastChanged" timestamp without time zone NOT NULL DEFAULT NOW(),
|
||||
"StatusFlag" smallint NOT NULL,
|
||||
@ -16,4 +17,7 @@ ALTER TABLE IF EXISTS public.users
|
||||
OWNER to postgres;
|
||||
|
||||
COMMENT ON TABLE public.users
|
||||
IS 'Table containing user information';
|
||||
IS 'Table containing user information';
|
||||
|
||||
COMMENT ON COLUMN public.users."PrivateKey"
|
||||
IS 'Private Key of the user with which the user can sign things';
|
19
src/migrations/06_publc_keys.sql
Normal file
19
src/migrations/06_publc_keys.sql
Normal file
@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS public."PublicKeys"
|
||||
(
|
||||
"Timestamp" timestamp without time zone NOT NULL DEFAULT now(),
|
||||
"UserID" character varying COLLATE pg_catalog."default" NOT NULL,
|
||||
"PublicKey" text COLLATE pg_catalog."default" NOT NULL,
|
||||
CONSTRAINT "PublicKeys_pkey" PRIMARY KEY ("Timestamp", "UserID"),
|
||||
CONSTRAINT "PublicKeyUserID" FOREIGN KEY ("UserID")
|
||||
REFERENCES public.users ("UserID") MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE NO ACTION
|
||||
)
|
||||
|
||||
TABLESPACE pg_default;
|
||||
|
||||
ALTER TABLE IF EXISTS public."PublicKeys"
|
||||
OWNER to postgres;
|
||||
|
||||
COMMENT ON TABLE public."PublicKeys"
|
||||
IS 'Public Key Storage for Signatures';
|
Loading…
Reference in New Issue
Block a user