diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d1072db --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,64 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'giterated-daemon'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=giterated-daemon" + ], + "filter": { + "name": "giterated-daemon", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'giterated-daemon'", + "cargo": { + "args": [ + "build", + "--bin=giterated-daemon", + "--package=giterated-daemon" + ], + "filter": { + "name": "giterated-daemon", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'giterated-daemon'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=giterated-daemon", + "--package=giterated-daemon" + ], + "filter": { + "name": "giterated-daemon", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 79bd87f..d746627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tracing-subscriber = "0.3" jsonwebtoken = { version = "*", features = ["use_pem"]} log = "*" rand = "*" -rsa = {version = "*", features = ["sha2"]} +rsa = {version = "0.9", features = ["sha2"]} reqwest = "*" toml = { version = "0.7" } diff --git a/src/authentication.rs b/src/authentication.rs index 7c44cf4..50ed06e 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,12 +1,18 @@ -use std::{error::Error, time::SystemTime}; +use std::{error::Error, os::raw, time::SystemTime}; -use jsonwebtoken::{encode, Algorithm, EncodingKey}; +use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; use serde::{Deserialize, Serialize}; use tokio::{fs::File, io::AsyncReadExt}; use toml::Table; use crate::{ - messages::authentication::{AuthenticationTokenRequest, AuthenticationTokenResponse}, + messages::{ + authentication::{ + AuthenticationTokenRequest, AuthenticationTokenResponse, TokenExtensionRequest, + TokenExtensionResponse, + }, + InstanceAuthenticated, + }, model::{instance::Instance, user::User}, }; @@ -72,4 +78,91 @@ impl AuthenticationTokenGranter { Ok(AuthenticationTokenResponse { token }) } + + pub async fn extension_request( + &mut self, + raw_request: InstanceAuthenticated, + ) -> Result> { + let request = raw_request.inner().await; + + let server_public_key = { + let mut file = File::open(self.config["keys"]["public"].as_str().unwrap()) + .await + .unwrap(); + + let mut key = vec![]; + file.read_to_end(&mut key).await.unwrap(); + + key + }; + + let verification_key = DecodingKey::from_rsa_pem(&server_public_key).unwrap(); + + let data: TokenData = decode( + &request.token, + &verification_key, + &Validation::new(Algorithm::RS256), + ) + .unwrap(); + + info!("Token Extension Request Token validated"); + + let secret_key = self.config["authentication"]["secret_key"] + .as_str() + .unwrap(); + + if request.secret_key != secret_key { + error!("Incorrect secret key!"); + + panic!() + } + + let requester_public_key = public_key(&data.claims.generated_for).await.unwrap(); + + // Validate request + raw_request.validate(requester_public_key).await.unwrap(); + info!("Validated request for key extension"); + + let private_key = { + let mut file = File::open(self.config["keys"]["private"].as_str().unwrap()) + .await + .unwrap(); + + let mut key = vec![]; + file.read_to_end(&mut key).await.unwrap(); + + key + }; + + let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); + + let claims = UserTokenMetadata { + // TODO: Probably exploitable + user: data.claims.user, + generated_for: data.claims.generated_for, + exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() + + std::time::Duration::from_secs(24 * 60 * 60)) + .as_secs(), + }; + + let token = encode( + &jsonwebtoken::Header::new(Algorithm::RS256), + &claims, + &encoding_key, + ) + .unwrap(); + + Ok(TokenExtensionResponse { + new_token: Some(token), + }) + } +} + +async fn public_key(instance: &Instance) -> Result> { + let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) + .await? + .text() + .await?; + + Ok(key) } diff --git a/src/backend/git.rs b/src/backend/git.rs index 01b2397..7575e40 100644 --- a/src/backend/git.rs +++ b/src/backend/git.rs @@ -5,7 +5,7 @@ use std::error::Error; use std::path::{Path, PathBuf}; use thiserror::Error; -use crate::messages::Authenticated; +use crate::messages::UserAuthenticated; use crate::model::instance::Instance; use crate::model::repository::{ Commit, RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, @@ -214,7 +214,7 @@ impl GitBackend { impl RepositoryBackend for GitBackend { async fn create_repository( &mut self, - raw_request: &Authenticated, + raw_request: &UserAuthenticated, ) -> Result> { let request = raw_request.inner().await; @@ -224,7 +224,14 @@ impl RepositoryBackend for GitBackend { .await .unwrap(); - assert!(matches!(raw_request.validate(public_key).await, Ok(()))); + match raw_request.validate(public_key).await { + Ok(_) => info!("Request was validated"), + Err(err) => { + error!("Failed to validate request: {:?}", err); + panic!(); + } + } + info!("Request was valid!"); // Check if repository already exists in the database diff --git a/src/backend/mod.rs b/src/backend/mod.rs index b3729ad..47e8728 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -16,7 +16,7 @@ use crate::{ UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, UserDisplayNameRequest, UserDisplayNameResponse, }, - Authenticated, + UserAuthenticated, }, model::repository::RepositoryView, }; @@ -25,7 +25,7 @@ use crate::{ pub trait RepositoryBackend: IssuesBackend { async fn create_repository( &mut self, - request: &Authenticated, + request: &UserAuthenticated, ) -> Result>; async fn repository_info( &mut self, diff --git a/src/connection.rs b/src/connection.rs index a9cc3c6..c6e4872 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -17,7 +17,7 @@ use crate::{ handshake::{HandshakeFinalize, HandshakeMessage, HandshakeResponse}, listener::Listeners, messages::{ - authentication::{AuthenticationMessage, AuthenticationRequest}, + authentication::{AuthenticationMessage, AuthenticationRequest, TokenExtensionResponse}, repository::{ RepositoryMessage, RepositoryMessageKind, RepositoryRequest, RepositoryResponse, }, @@ -61,7 +61,7 @@ pub async fn connection_worker( ) { let mut handshaked = false; let this_instance = Instance { - url: String::from("127.0.0.1:8080"), + url: String::from("giterated.dev"), }; while let Some(message) = socket.next().await { @@ -389,6 +389,26 @@ pub async fn connection_worker( .unwrap(); continue; } + AuthenticationRequest::TokenExtension(request) => { + let mut granter = auth_granter.lock().await; + + let response = granter + .extension_request(request.clone()) + .await + .unwrap_or_else(|_| TokenExtensionResponse { new_token: None }); + drop(granter); + + socket + .send(Message::Binary( + serde_json::to_vec(&MessageKind::Authentication( + AuthenticationMessage::Response(crate::messages::authentication::AuthenticationResponse::TokenExtension(response)) + )) + .unwrap(), + )) + .await + .unwrap(); + continue; + } }, AuthenticationMessage::Response(_) => unreachable!(), } diff --git a/src/main.rs b/src/main.rs index 8f4ecea..f691d42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,9 @@ async fn main() -> Result<(), Box> { .password(config["postgres"]["password"].as_str().unwrap()) .log_statements(log::LevelFilter::Off); let db_pool = PgPool::connect_with(db_conn_options).await?; + + debug!("Running database migrations..."); + sqlx::migrate!().run(&db_pool).await?; info!("Connected"); let repository_backend: Arc> = Arc::new(Mutex::new({ diff --git a/src/messages/authentication.rs b/src/messages/authentication.rs index a095ba0..f86e6c6 100644 --- a/src/messages/authentication.rs +++ b/src/messages/authentication.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use super::InstanceAuthenticated; + #[derive(Clone, Serialize, Deserialize)] pub enum AuthenticationMessage { Request(AuthenticationRequest), @@ -9,11 +11,13 @@ pub enum AuthenticationMessage { #[derive(Clone, Serialize, Deserialize)] pub enum AuthenticationRequest { AuthenticationToken(AuthenticationTokenRequest), + TokenExtension(InstanceAuthenticated), } #[derive(Clone, Serialize, Deserialize)] pub enum AuthenticationResponse { AuthenticationToken(AuthenticationTokenResponse), + TokenExtension(TokenExtensionResponse), } #[derive(Clone, Serialize, Deserialize)] @@ -27,3 +31,14 @@ pub struct AuthenticationTokenRequest { pub struct AuthenticationTokenResponse { pub token: String, } + +#[derive(Clone, Serialize, Deserialize)] +pub struct TokenExtensionRequest { + pub secret_key: String, + pub token: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct TokenExtensionResponse { + pub new_token: Option, +} diff --git a/src/messages/mod.rs b/src/messages/mod.rs index 0406660..e6820da 100644 --- a/src/messages/mod.rs +++ b/src/messages/mod.rs @@ -1,15 +1,16 @@ use std::{error::Error, fmt::Debug}; use rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, pkcs8::{DecodePrivateKey, DecodePublicKey}, pss::{Signature, SigningKey, VerifyingKey}, sha2::Sha256, signature::{RandomizedSigner, SignatureEncoding, Verifier}, - RsaPrivateKey, RsaPublicKey, pkcs1::DecodeRsaPrivateKey, + RsaPrivateKey, RsaPublicKey, }; use serde::{Deserialize, Serialize}; -use crate::handshake::HandshakeMessage; +use crate::{handshake::HandshakeMessage, model::instance::Instance}; use self::{authentication::AuthenticationMessage, repository::RepositoryMessage}; @@ -25,19 +26,98 @@ pub enum MessageKind { Authentication(AuthenticationMessage), } +/// An authenticated message, where the instance is authenticating +/// a request it is making for itself. +#[derive(Serialize, Deserialize)] +pub struct InstanceAuthenticated { + message: T, + instance: Instance, + signature: Vec, +} + +impl Clone for InstanceAuthenticated +where + T: Clone + Serialize, +{ + fn clone(&self) -> Self { + Self { + message: self.message.clone(), + instance: self.instance.clone(), + signature: self.signature.clone(), + } + } +} + +impl Debug for InstanceAuthenticated +where + T: Debug + Serialize, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Authenticated") + .field("message", &self.message) + .field("instance", &self.instance) + .field("signature", &self.signature) + .finish() + } +} + +impl InstanceAuthenticated { + pub fn new( + message: T, + instance: Instance, + private_key: String, + ) -> Result> { + let mut rng = rand::thread_rng(); + + let private_key = RsaPrivateKey::from_pkcs1_pem(&private_key)?; + let signing_key = SigningKey::::new(private_key); + + let message_json = serde_json::to_vec(&message)?; + + let signature = signing_key.sign_with_rng(&mut rng, &message_json); + + Ok(Self { + message, + instance, + signature: signature.to_vec(), + }) + } + + pub async fn inner(&self) -> &T { + &self.message + } + + pub async fn validate(&self, key: String) -> Result<(), Box> { + let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); + + let verifying_key: VerifyingKey = VerifyingKey::new(public_key); + + let message_json = serde_json::to_vec(&self.message).unwrap(); + + verifying_key + .verify( + &message_json, + &Signature::try_from(self.signature.as_ref()).unwrap(), + ) + .unwrap(); + + Ok(()) + } +} + /// An authenticated message. /// /// Includes the message, with a digest generated with /// our private key. #[derive(Serialize, Deserialize)] -pub struct Authenticated { +pub struct UserAuthenticated { #[serde(flatten)] message: T, token: String, digest: Vec, } -impl Clone for Authenticated +impl Clone for UserAuthenticated where T: Clone + Serialize, { @@ -50,7 +130,7 @@ where } } -impl Debug for Authenticated +impl Debug for UserAuthenticated where T: Debug + Serialize, { @@ -63,7 +143,7 @@ where } } -impl Authenticated { +impl UserAuthenticated { pub fn new(message: T, token: String, private_key: String) -> Result> { let mut rng = rand::thread_rng(); @@ -86,13 +166,18 @@ impl Authenticated { } pub async fn validate(&self, key: String) -> Result<(), Box> { - let public_key = RsaPublicKey::from_public_key_pem(&key)?; + let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); let verifying_key: VerifyingKey = VerifyingKey::new(public_key); - let message_json = serde_json::to_vec(&self.message)?; + let message_json = serde_json::to_vec(&self.message).unwrap(); - verifying_key.verify(&message_json, &Signature::try_from(self.digest.as_ref())?)?; + verifying_key + .verify( + &message_json, + &Signature::try_from(self.digest.as_ref()).unwrap(), + ) + .unwrap(); Ok(()) } diff --git a/src/messages/repository.rs b/src/messages/repository.rs index 3e067c8..cd9f4c0 100644 --- a/src/messages/repository.rs +++ b/src/messages/repository.rs @@ -6,7 +6,7 @@ use crate::model::{ user::User, }; -use super::Authenticated; +use super::UserAuthenticated; #[derive(Clone, Serialize, Deserialize)] pub struct RepositoryMessage { @@ -22,7 +22,7 @@ pub enum RepositoryMessageKind { #[derive(Clone, Serialize, Deserialize)] pub enum RepositoryRequest { - CreateRepository(Authenticated), + CreateRepository(UserAuthenticated), RepositoryFileInspect(RepositoryFileInspectRequest), RepositoryInfo(RepositoryInfoRequest), IssuesCount(RepositoryIssuesCountRequest),