2023-04-10 13:02:25 +03:00
/*
* This file is part of mailpot
*
* Copyright 2020 - Manos Pitsidianakis
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https ://www.gnu.org/licenses/>.
* /
2023-04-15 17:25:37 +03:00
use std ::{ borrow ::Cow , process ::Stdio } ;
2023-04-10 13:02:25 +03:00
use tempfile ::NamedTempFile ;
2023-04-15 17:25:37 +03:00
use tokio ::{ fs ::File , io ::AsyncWriteExt , process ::Command } ;
2023-04-10 13:02:25 +03:00
2023-04-15 17:25:37 +03:00
use super ::* ;
2023-04-10 13:02:25 +03:00
const TOKEN_KEY : & str = " ssh_challenge " ;
const EXPIRY_IN_SECS : i64 = 6 * 60 ;
#[ derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, PartialOrd) ]
pub enum Role {
User ,
Admin ,
}
#[ derive(serde::Serialize, serde::Deserialize, Debug, Clone) ]
pub struct User {
2023-04-14 14:30:05 +03:00
/// SSH signature.
pub ssh_signature : String ,
/// User role.
2023-04-10 13:02:25 +03:00
pub role : Role ,
2023-04-14 14:30:05 +03:00
/// Database primary key.
pub pk : i64 ,
/// Accounts's display name, optional.
pub name : Option < String > ,
/// Account's e-mail address.
2023-04-10 13:02:25 +03:00
pub address : String ,
2023-04-14 14:30:05 +03:00
/// GPG public key.
pub public_key : Option < String > ,
/// SSH public key.
pub password : String ,
/// Whether this account is enabled.
pub enabled : bool ,
2023-04-10 13:02:25 +03:00
}
impl AuthUser < i64 , Role > for User {
fn get_id ( & self ) -> i64 {
2023-04-14 14:30:05 +03:00
self . pk
2023-04-10 13:02:25 +03:00
}
fn get_password_hash ( & self ) -> SecretVec < u8 > {
2023-04-14 14:30:05 +03:00
SecretVec ::new ( self . ssh_signature . clone ( ) . into ( ) )
2023-04-10 13:02:25 +03:00
}
fn get_role ( & self ) -> Option < Role > {
Some ( self . role )
}
}
#[ derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default) ]
pub struct AuthFormPayload {
pub address : String ,
pub password : String ,
}
pub async fn ssh_signin (
2023-04-15 17:25:37 +03:00
_ : LoginPath ,
2023-04-10 13:02:25 +03:00
mut session : WritableSession ,
2023-04-15 13:35:12 +03:00
Query ( next ) : Query < Next > ,
2023-04-10 13:02:25 +03:00
auth : AuthContext ,
State ( state ) : State < Arc < AppState > > ,
) -> impl IntoResponse {
if auth . current_user . is_some ( ) {
2023-04-13 18:47:08 +03:00
if let Err ( err ) = session . add_message ( Message {
message : " You are already logged in. " . into ( ) ,
level : Level ::Info ,
} ) {
return err . into_response ( ) ;
}
2023-04-15 13:35:12 +03:00
return next
2023-04-15 17:25:37 +03:00
. or_else ( | | format! ( " {} {} " , state . root_url_prefix , SettingsPath . to_uri ( ) ) )
2023-04-15 13:35:12 +03:00
. into_response ( ) ;
}
if next . next . is_some ( ) {
if let Err ( err ) = session . add_message ( Message {
message : " You need to be logged in to access this page. " . into ( ) ,
level : Level ::Info ,
} ) {
return err . into_response ( ) ;
} ;
2023-04-10 13:02:25 +03:00
}
let now : i64 = chrono ::offset ::Utc ::now ( ) . timestamp ( ) ;
let prev_token = if let Some ( tok ) = session . get ::< ( String , i64 ) > ( TOKEN_KEY ) {
let timestamp : i64 = tok . 1 ;
if ! ( timestamp < now & & now - timestamp < EXPIRY_IN_SECS ) {
session . remove ( TOKEN_KEY ) ;
None
} else {
Some ( tok )
}
} else {
None
} ;
let ( token , timestamp ) : ( String , i64 ) = if let Some ( tok ) = prev_token {
tok
} else {
2023-04-15 17:25:37 +03:00
use rand ::{ distributions ::Alphanumeric , thread_rng , Rng } ;
2023-04-10 13:02:25 +03:00
let mut rng = thread_rng ( ) ;
let chars : String = ( 0 .. 7 ) . map ( | _ | rng . sample ( Alphanumeric ) as char ) . collect ( ) ;
println! ( " Random chars: {} " , chars ) ;
session . insert ( TOKEN_KEY , ( & chars , now ) ) . unwrap ( ) ;
( chars , now )
} ;
let timeout_left = ( ( timestamp + EXPIRY_IN_SECS ) - now ) as f64 / 60.0 ;
let root_url_prefix = & state . root_url_prefix ;
let crumbs = vec! [
Crumb {
2023-04-15 17:25:37 +03:00
label : " Home " . into ( ) ,
2023-04-10 13:02:25 +03:00
url : " / " . into ( ) ,
} ,
Crumb {
label : " Sign in " . into ( ) ,
2023-04-15 17:25:37 +03:00
url : LoginPath . to_crumb ( ) ,
2023-04-10 13:02:25 +03:00
} ,
] ;
let context = minijinja ::context! {
namespace = > & state . public_url ,
2023-04-14 15:46:45 +03:00
title = > state . site_title . as_ref ( ) ,
page_title = > " Log in " ,
2023-04-10 13:02:25 +03:00
description = > " " ,
root_url_prefix = > & root_url_prefix ,
ssh_challenge = > token ,
timeout_left = > timeout_left ,
current_user = > auth . current_user ,
2023-04-13 18:47:08 +03:00
messages = > session . drain_messages ( ) ,
2023-04-10 13:02:25 +03:00
crumbs = > crumbs ,
} ;
Html (
TEMPLATES
. get_template ( " auth.html " )
. unwrap ( )
. render ( context )
. unwrap_or_else ( | err | err . to_string ( ) ) ,
)
. into_response ( )
}
pub async fn ssh_signin_post (
2023-04-15 17:25:37 +03:00
_ : LoginPath ,
2023-04-13 18:47:08 +03:00
mut session : WritableSession ,
2023-04-15 13:35:12 +03:00
Query ( next ) : Query < Next > ,
2023-04-10 13:02:25 +03:00
mut auth : AuthContext ,
Form ( payload ) : Form < AuthFormPayload > ,
state : Arc < AppState > ,
) -> Result < Redirect , ResponseError > {
if auth . current_user . as_ref ( ) . is_some ( ) {
2023-04-13 18:47:08 +03:00
session . add_message ( Message {
message : " You are already logged in. " . into ( ) ,
level : Level ::Info ,
} ) ? ;
2023-04-15 17:25:37 +03:00
return Ok ( next . or_else ( | | format! ( " {} {} " , state . root_url_prefix , SettingsPath . to_uri ( ) ) ) ) ;
2023-04-10 13:02:25 +03:00
}
let now : i64 = chrono ::offset ::Utc ::now ( ) . timestamp ( ) ;
let ( prev_token , _ ) =
if let Some ( tok @ ( _ , timestamp ) ) = session . get ::< ( String , i64 ) > ( TOKEN_KEY ) {
if ! ( timestamp < now & & now - timestamp < EXPIRY_IN_SECS ) {
2023-04-13 18:47:08 +03:00
session . add_message ( Message {
message : " The token has expired. Please retry. " . into ( ) ,
level : Level ::Error ,
} ) ? ;
2023-04-15 13:35:12 +03:00
return Ok ( Redirect ::to ( & format! (
2023-04-15 17:25:37 +03:00
" {}{}{} " ,
2023-04-15 13:35:12 +03:00
state . root_url_prefix ,
2023-04-15 17:25:37 +03:00
LoginPath . to_uri ( ) ,
2023-04-15 13:35:12 +03:00
if let Some ( ref next ) = next . next {
next . as_str ( )
} else {
" "
}
) ) ) ;
2023-04-10 13:02:25 +03:00
} else {
tok
}
} else {
2023-04-13 18:47:08 +03:00
session . add_message ( Message {
message : " The token has expired. Please retry. " . into ( ) ,
level : Level ::Error ,
} ) ? ;
2023-04-15 13:35:12 +03:00
return Ok ( Redirect ::to ( & format! (
2023-04-15 17:25:37 +03:00
" {}{}{} " ,
2023-04-15 13:35:12 +03:00
state . root_url_prefix ,
2023-04-15 17:25:37 +03:00
LoginPath . to_uri ( ) ,
2023-04-15 13:35:12 +03:00
if let Some ( ref next ) = next . next {
next . as_str ( )
} else {
" "
}
) ) ) ;
2023-04-10 13:02:25 +03:00
} ;
drop ( session ) ;
let db = Connection ::open_db ( state . conf . clone ( ) ) ? ;
2023-04-14 14:30:05 +03:00
let mut acc = match db
2023-04-10 13:02:25 +03:00
. account_by_address ( & payload . address )
. with_status ( StatusCode ::BAD_REQUEST ) ?
{
Some ( v ) = > v ,
None = > {
return Err ( ResponseError ::new (
format! ( " Account for {} not found " , payload . address ) ,
StatusCode ::NOT_FOUND ,
) ) ;
}
} ;
let sig = SshSignature {
email : payload . address . clone ( ) ,
ssh_public_key : acc . password . clone ( ) ,
ssh_signature : payload . password . clone ( ) ,
2023-04-14 22:46:22 +03:00
namespace : std ::env ::var ( " SSH_NAMESPACE " )
. unwrap_or_else ( | _ | " lists.mailpot.rs " . to_string ( ) )
. into ( ) ,
2023-04-10 13:02:25 +03:00
token : prev_token ,
} ;
ssh_keygen ( sig ) . await ? ;
let user = User {
2023-04-14 14:30:05 +03:00
pk : acc . pk ( ) ,
ssh_signature : payload . password ,
2023-04-10 13:02:25 +03:00
role : Role ::User ,
2023-04-14 14:30:05 +03:00
public_key : std ::mem ::take ( & mut acc . public_key ) ,
password : std ::mem ::take ( & mut acc . password ) ,
name : std ::mem ::take ( & mut acc . name ) ,
2023-04-10 13:02:25 +03:00
address : payload . address ,
2023-04-14 14:30:05 +03:00
enabled : acc . enabled ,
2023-04-10 13:02:25 +03:00
} ;
state . insert_user ( acc . pk ( ) , user . clone ( ) ) . await ;
auth . login ( & user )
. await
. map_err ( | err | ResponseError ::new ( err . to_string ( ) , StatusCode ::BAD_REQUEST ) ) ? ;
2023-04-15 17:25:37 +03:00
Ok ( next . or_else ( | | format! ( " {} {} " , state . root_url_prefix , SettingsPath . to_uri ( ) ) ) )
2023-04-10 13:02:25 +03:00
}
#[ derive(Debug, Clone, Default) ]
pub struct SshSignature {
pub email : String ,
pub ssh_public_key : String ,
pub ssh_signature : String ,
pub namespace : Cow < 'static , str > ,
pub token : String ,
}
/// Run ssh signature validation with `ssh-keygen` binary.
///
/// ```no_run
2023-04-14 15:54:34 +03:00
/// use mailpot_web::{ssh_keygen, SshSignature};
2023-04-10 13:02:25 +03:00
///
/// async fn key_gen(
/// ssh_public_key: String,
/// ssh_signature: String,
/// ) -> std::result::Result<(), Box<dyn std::error::Error>> {
/// let mut sig = SshSignature {
/// email: "user@example.com".to_string(),
/// ssh_public_key,
/// ssh_signature,
/// namespace: "doc-test@example.com".into(),
/// token: "d074a61990".to_string(),
/// };
///
/// ssh_keygen(sig.clone()).await?;
/// Ok(())
/// }
/// ```
2023-04-13 22:25:20 +03:00
pub async fn ssh_keygen ( sig : SshSignature ) -> Result < ( ) , Box < dyn std ::error ::Error > > {
2023-04-10 13:02:25 +03:00
let SshSignature {
email ,
ssh_public_key ,
ssh_signature ,
namespace ,
token ,
} = sig ;
let dir = tempfile ::tempdir ( ) ? ;
let mut allowed_signers_fp = NamedTempFile ::new_in ( dir . path ( ) ) ? ;
let mut signature_fp = NamedTempFile ::new_in ( dir . path ( ) ) ? ;
{
let ( tempfile , path ) = allowed_signers_fp . into_parts ( ) ;
let mut file = File ::from ( tempfile ) ;
file . write_all ( format! ( " {email} {ssh_public_key} " ) . as_bytes ( ) )
. await ? ;
file . flush ( ) . await ? ;
allowed_signers_fp = NamedTempFile ::from_parts ( file . into_std ( ) . await , path ) ;
}
{
let ( tempfile , path ) = signature_fp . into_parts ( ) ;
let mut file = File ::from ( tempfile ) ;
file . write_all ( ssh_signature . trim ( ) . replace ( " \r \n " , " \n " ) . as_bytes ( ) )
. await ? ;
file . flush ( ) . await ? ;
signature_fp = NamedTempFile ::from_parts ( file . into_std ( ) . await , path ) ;
}
let mut cmd = Command ::new ( " ssh-keygen " ) ;
cmd . stdout ( Stdio ::piped ( ) ) ;
cmd . stderr ( Stdio ::piped ( ) ) ;
cmd . stdin ( Stdio ::piped ( ) ) ;
// Once you have your allowed signers file, verification works like this:
// ssh-keygen -Y verify -f allowed_signers -I alice@example.com -n file -s file_to_verify.sig < file_to_verify
// Here are the arguments you may need to change:
// allowed_signers is the path to the allowed signers file.
// alice@example.com is the email address of the person who allegedly signed the file. This email address is looked up in the allowed signers file to get possible public keys.
// file is the "namespace", which must match the namespace used for signing as described above.
// file_to_verify.sig is the path to the signature file.
// file_to_verify is the path to the file to be verified. Note that this file is read from standard in. In the above command, the < shell operator is used to redirect standard in from this file.
// If the signature is valid, the command exits with status 0 and prints a message like this:
// Good "file" signature for alice@example.com with ED25519 key SHA256:ZGa8RztddW4kE2XKPPsP9ZYC7JnMObs6yZzyxg8xZSk
// Otherwise, the command exits with a non-zero status and prints an error message.
let mut child = cmd
. arg ( " -Y " )
. arg ( " verify " )
. arg ( " -f " )
. arg ( allowed_signers_fp . path ( ) )
. arg ( " -I " )
. arg ( & email )
. arg ( " -n " )
. arg ( namespace . as_ref ( ) )
. arg ( " -s " )
. arg ( signature_fp . path ( ) )
. spawn ( )
. expect ( " failed to spawn command " ) ;
let mut stdin = child
. stdin
. take ( )
. expect ( " child did not have a handle to stdin " ) ;
stdin
. write_all ( token . as_bytes ( ) )
. await
. expect ( " could not write to stdin " ) ;
drop ( stdin ) ;
let op = child . wait_with_output ( ) . await ? ;
if ! op . status . success ( ) {
2023-04-13 22:25:20 +03:00
return Err ( format! (
" ssh-keygen exited with {}: \n stdout: {} \n \n stderr: {} " ,
op . status . code ( ) . unwrap_or ( - 1 ) ,
String ::from_utf8_lossy ( & op . stdout ) ,
String ::from_utf8_lossy ( & op . stderr )
)
. into ( ) ) ;
2023-04-10 13:02:25 +03:00
}
Ok ( ( ) )
}
2023-04-15 17:25:37 +03:00
pub async fn logout_handler (
_ : LogoutPath ,
mut auth : AuthContext ,
State ( state ) : State < Arc < AppState > > ,
) -> Redirect {
2023-04-10 13:02:25 +03:00
auth . logout ( ) . await ;
2023-04-15 17:25:37 +03:00
Redirect ::to ( & format! ( " {} / " , state . root_url_prefix ) )
2023-04-10 13:02:25 +03:00
}
2023-04-15 13:35:12 +03:00
pub mod auth_request {
2023-04-15 17:25:37 +03:00
use std ::{ marker ::PhantomData , ops ::RangeBounds } ;
2023-04-15 13:35:12 +03:00
use axum ::body ::HttpBody ;
use dyn_clone ::DynClone ;
use tower_http ::auth ::AuthorizeRequest ;
2023-04-15 17:25:37 +03:00
use super ::* ;
2023-04-15 13:35:12 +03:00
trait RoleBounds < Role > : DynClone + Send + Sync {
fn contains ( & self , role : Option < Role > ) -> bool ;
}
impl < T , Role > RoleBounds < Role > for T
where
Role : PartialOrd + PartialEq ,
T : RangeBounds < Role > + Clone + Send + Sync ,
{
fn contains ( & self , role : Option < Role > ) -> bool {
if let Some ( role ) = role {
RangeBounds ::contains ( self , & role )
} else {
role . is_none ( )
}
}
}
/// Type that performs login authorization.
///
/// See [`RequireAuthorizationLayer::login`] for more details.
pub struct Login < UserId , User , ResBody , Role = ( ) > {
login_url : Option < Arc < Cow < 'static , str > > > ,
redirect_field_name : Option < Arc < Cow < 'static , str > > > ,
role_bounds : Box < dyn RoleBounds < Role > > ,
_user_id_type : PhantomData < UserId > ,
_user_type : PhantomData < User > ,
_body_type : PhantomData < fn ( ) -> ResBody > ,
}
impl < UserId , User , ResBody , Role > Clone for Login < UserId , User , ResBody , Role > {
fn clone ( & self ) -> Self {
Self {
login_url : self . login_url . clone ( ) ,
redirect_field_name : self . redirect_field_name . clone ( ) ,
role_bounds : dyn_clone ::clone_box ( & * self . role_bounds ) ,
_user_id_type : PhantomData ,
_user_type : PhantomData ,
_body_type : PhantomData ,
}
}
}
impl < UserId , User , ReqBody , ResBody , Role > AuthorizeRequest < ReqBody >
for Login < UserId , User , ResBody , Role >
where
Role : PartialOrd + PartialEq + Clone + Send + Sync + 'static ,
User : AuthUser < UserId , Role > ,
ResBody : HttpBody + Default ,
{
type ResponseBody = ResBody ;
fn authorize (
& mut self ,
request : & mut Request < ReqBody > ,
) -> Result < ( ) , Response < Self ::ResponseBody > > {
let user = request
. extensions ( )
. get ::< Option < User > > ( )
. expect ( " Auth extension missing. Is the auth layer installed? " ) ;
match user {
Some ( user ) if self . role_bounds . contains ( user . get_role ( ) ) = > {
let user = user . clone ( ) ;
request . extensions_mut ( ) . insert ( user ) ;
Ok ( ( ) )
}
_ = > {
let unauthorized_response = if let Some ( ref login_url ) = self . login_url {
let url : Cow < 'static , str > =
if let Some ( ref next ) = self . redirect_field_name {
format! (
" {login_url}?{next}={} " ,
percent_encoding ::utf8_percent_encode (
request . uri ( ) . path ( ) ,
percent_encoding ::CONTROLS
)
)
. into ( )
} else {
login_url . as_ref ( ) . clone ( )
} ;
Response ::builder ( )
. status ( http ::StatusCode ::TEMPORARY_REDIRECT )
. header ( http ::header ::LOCATION , url . as_ref ( ) )
. body ( Default ::default ( ) )
. unwrap ( )
} else {
Response ::builder ( )
. status ( http ::StatusCode ::UNAUTHORIZED )
. body ( Default ::default ( ) )
. unwrap ( )
} ;
Err ( unauthorized_response )
}
}
}
}
/// A wrapper around [`tower_http::auth::RequireAuthorizationLayer`] which
/// provides login authorization.
pub struct RequireAuthorizationLayer < UserId , User , Role = ( ) > ( UserId , User , Role ) ;
impl < UserId , User , Role > RequireAuthorizationLayer < UserId , User , Role >
where
Role : PartialOrd + PartialEq + Clone + Send + Sync + 'static ,
User : AuthUser < UserId , Role > ,
{
2023-04-15 17:25:37 +03:00
/// Authorizes requests by requiring a logged in user, otherwise it
/// rejects with [`http::StatusCode::UNAUTHORIZED`].
2023-04-15 13:35:12 +03:00
pub fn login < ResBody > (
) -> tower_http ::auth ::RequireAuthorizationLayer < Login < UserId , User , ResBody , Role > >
where
ResBody : HttpBody + Default ,
{
tower_http ::auth ::RequireAuthorizationLayer ::custom ( Login ::< _ , _ , _ , _ > {
login_url : None ,
redirect_field_name : None ,
role_bounds : Box ::new ( .. ) ,
_user_id_type : PhantomData ,
_user_type : PhantomData ,
_body_type : PhantomData ,
} )
}
/// Authorizes requests by requiring a logged in user to have a specific
/// range of roles, otherwise it rejects with
/// [`http::StatusCode::UNAUTHORIZED`].
pub fn login_with_role < ResBody > (
role_bounds : impl RangeBounds < Role > + Clone + Send + Sync + 'static ,
) -> tower_http ::auth ::RequireAuthorizationLayer < Login < UserId , User , ResBody , Role > >
where
ResBody : HttpBody + Default ,
{
tower_http ::auth ::RequireAuthorizationLayer ::custom ( Login ::< _ , _ , _ , _ > {
login_url : None ,
redirect_field_name : None ,
role_bounds : Box ::new ( role_bounds ) ,
_user_id_type : PhantomData ,
_user_type : PhantomData ,
_body_type : PhantomData ,
} )
}
2023-04-15 17:25:37 +03:00
/// Authorizes requests by requiring a logged in user, otherwise it
/// redirects to the provided login URL.
2023-04-15 13:35:12 +03:00
///
2023-04-15 17:25:37 +03:00
/// If `redirect_field_name` is set to a value, the login page will
/// receive the path it was redirected from in the URI query
/// part. For example, attempting to visit a protected path
/// `/protected` would redirect you to `/login?next=/protected` allowing
/// you to know how to return the visitor to their requested
/// page.
2023-04-15 13:35:12 +03:00
pub fn login_or_redirect < ResBody > (
login_url : Arc < Cow < 'static , str > > ,
redirect_field_name : Option < Arc < Cow < 'static , str > > > ,
) -> tower_http ::auth ::RequireAuthorizationLayer < Login < UserId , User , ResBody , Role > >
where
ResBody : HttpBody + Default ,
{
tower_http ::auth ::RequireAuthorizationLayer ::custom ( Login ::< _ , _ , _ , _ > {
login_url : Some ( login_url ) ,
redirect_field_name ,
role_bounds : Box ::new ( .. ) ,
_user_id_type : PhantomData ,
_user_type : PhantomData ,
_body_type : PhantomData ,
} )
}
/// Authorizes requests by requiring a logged in user to have a specific
/// range of roles, otherwise it redirects to the
/// provided login URL.
///
2023-04-15 17:25:37 +03:00
/// If `redirect_field_name` is set to a value, the login page will
/// receive the path it was redirected from in the URI query
/// part. For example, attempting to visit a protected path
/// `/protected` would redirect you to `/login?next=/protected` allowing
/// you to know how to return the visitor to their requested
/// page.
2023-04-15 13:35:12 +03:00
pub fn login_with_role_or_redirect < ResBody > (
role_bounds : impl RangeBounds < Role > + Clone + Send + Sync + 'static ,
login_url : Arc < Cow < 'static , str > > ,
redirect_field_name : Option < Arc < Cow < 'static , str > > > ,
) -> tower_http ::auth ::RequireAuthorizationLayer < Login < UserId , User , ResBody , Role > >
where
ResBody : HttpBody + Default ,
{
tower_http ::auth ::RequireAuthorizationLayer ::custom ( Login ::< _ , _ , _ , _ > {
login_url : Some ( login_url ) ,
redirect_field_name ,
role_bounds : Box ::new ( role_bounds ) ,
_user_id_type : PhantomData ,
_user_type : PhantomData ,
_body_type : PhantomData ,
} )
}
}
}
2023-04-10 13:02:25 +03:00
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ tokio::test ]
async fn test_ssh_keygen ( ) {
const PKEY : & str = concat! ( " ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCzXp8nLJL8GPNw7S+Dqt0m3Dw/ " ,
" xFOAdwKXcekTFI9cLDEUII2rNPf0uUZTpv57OgU+ " ,
" QOEEIvWMjz+5KSWBX8qdP8OtV0QNvynlZkEKZN0cUqGKaNXo5a+PUDyiJ2rHroPe1aMo6mUBL9kLR6J2U1CYD/dLfL8ywXsAGmOL0bsK0GRPVBJAjpUNRjpGU/ " ,
" 2FFIlU6s6GawdbDXEHDox/UoOVAKIlhKabaTrFBA0ACFLRX2/GCBmHqqt5d4ZZjefYzReLs/beOjafYImoyhHC428wZDcUjvLrpSJbIOE/ " ,
" gSPCWlRbcsxg4JGcKOtALUurE+ok+avy9M7eFjGhLGSlTKLdshIVQr/3W667M7bYfOT6xP/ " ,
" lyjxeWIUYyj7rjlqKJ9tzygek7QNxCtuqH5xsZAZqzQCN8wfrPAlwDykvWityKOw+Bt2DWjimITqyKgsBsOaA+ " ,
" eVCllFvooJxoYvAjODASjAUoOdgVzyBDpFnOhLFYiIIyL3F6NROS9i7z086paX7mrzcQzvLr4ckF9qT7DrI88ikISCR9bFR4vPq3aH " ,
" zJdjDDpWxACa5b11NG8KdCJPe/L0kDw82Q00U13CpW9FI9sZjvk+ " ,
" lyw8bTFvVsIl6A0ueboFvrNvznAqHrtfWu75fXRh5sKj2TGk8rhm3vyNgrBSr5zAfFVM8LgqBxbAAYw== " ) ;
const SIG : & str = concat! (
" -----BEGIN SSH SIGNATURE----- \n " ,
" U1NIU0lHAAAAAQAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBALNenycskvwY83DtL4Oq3S \n " ,
" bcPD/EU4B3Apdx6RMUj1wsMRQgjas09/S5RlOm/ns6BT5A4QQi9YyPP7kpJYFfyp0/w61X \n " ,
" RA2/KeVmQQpk3RxSoYpo1ejlr49QPKInaseug97VoyjqZQEv2QtHonZTUJgP90t8vzLBew \n " ,
" AaY4vRuwrQZE9UEkCOlQ1GOkZT/YUUiVTqzoZrB1sNcQcOjH9Sg5UAoiWEpptpOsUEDQAI \n " ,
" UtFfb8YIGYeqq3l3hlmN59jNF4uz9t46Np9giajKEcLjbzBkNxSO8uulIlsg4T+BI8JaVF \n " ,
" tyzGDgkZwo60AtS6sT6iT5q/L0zt4WMaEsZKVMot2yEhVCv/dbrrsztth85PrE/+XKPF5Y \n " ,
" hRjKPuuOWoon23PKB6TtA3EK26ofnGxkBmrNAI3zB+s8CXAPKS9aK3Io7D4G3YNaOKYhOr \n " ,
" IqCwGw5oD55UKWUW+ignGhi8CM4MBKMBSg52BXPIEOkWc6EsViIgjIvcXo1E5L2LvPTzql \n " ,
" pfuavNxDO8uvhyQX2pPsOsjzyKQhIJH1sVHi8+rdofMl2MMOlbEAJrlvXU0bwp0Ik978vS \n " ,
" QPDzZDTRTXcKlb0Uj2xmO+T6XLDxtMW9WwiXoDS55ugW+s2/OcCoeu19a7vl9dGHmwqPZM \n " ,
" aTyuGbe/I2CsFKvnMB8VUzwuCoHFsABjAAAAFGRvYy10ZXN0QGV4YW1wbGUuY29tAAAAAA \n " ,
" AAAAZzaGE1MTIAAAIUAAAADHJzYS1zaGEyLTUxMgAAAgBxaMqIfeapKTrhQzggDssD+76s \n " ,
" jZxv3XxzgsuAjlIdtw+/nyxU6skTnrGoam2shpmQvx0HuqSQ7HyS2USBK7T4LZNoE53zR/ \n " ,
" ZmHLGoyQAoexiHSEW9Lk53kyRNPhpXQedTvm8REHPGM3zw6WO6mAXVVxvebvawf81LTbBb \n " ,
" p9ubNRcHgktVeywMO/sD6zWSyShq1gjVv1PdRBOjUgqkwjImL8dFKi1QUeoffCxyk3JhTO \n " ,
" siTy79HZSz/kOvkvL1vQuqaP2R8lE9P1uaD19dGOMTPRod3u+QmpYX47ri5KM3Fmkfxdwq \n " ,
" p8JVmfAA9nme7bmNS1hWgmF2Nbh9qjh1zOZvCimIpuNtz5eEl9K+1DxG6w5tX86wSGvBMO \n " ,
" znx0k1gGfkiAULqgrkdul7mqMPRvPN9J6QlNJ7SLFChRhzlJIJc6tOvCs7qkVD43Zcb+I5 \n " ,
" Z+K4NiFf5jf8kVX/pjjeW/ucbrctJIkGsZ58OkHKi1EDRcq7NtCF6SKlcv8g3fMLd9wW6K \n " ,
" aaed0TBDC+s+f6naNIGvWqfWCwDuK5xGyDTTmJGcrsMwWuT9K6uLk8cGdv7t5mOFuWi5jl \n " ,
" E+IKZKVABMuWqSj96ErMIiBjtsAZfNSezpsK49wQztoSPhdwLhD6fHrSAyPCqN2xRkcsIb \n " ,
" 6PxWKC/OELf3gyEBRPouxsF7xSZQ== \n " ,
" -----END SSH SIGNATURE----- \n "
) ;
const NAMESPACE : & str = " doc-test@example.com " ;
let mut sig = SshSignature {
email : " user@example.com " . to_string ( ) ,
ssh_public_key : PKEY . to_string ( ) ,
ssh_signature : SIG . to_string ( ) ,
namespace : " doc-test@example.com " . into ( ) ,
token : " d074a61990 " . to_string ( ) ,
} ;
ssh_keygen ( sig . clone ( ) ) . await . unwrap ( ) ;
2023-04-14 16:34:53 +03:00
sig . ssh_signature = sig . ssh_signature . replace ( 'J' , " 0 " ) ;
2023-04-10 13:02:25 +03:00
let err = ssh_keygen ( sig ) . await . unwrap_err ( ) ;
assert! (
err . to_string ( ) . starts_with ( " ssh-keygen exited with " ) ,
" {} " ,
err
) ;
}
}