Add token extension
parent: tbd commit: 86d028f
Showing 10 changed files with 309 insertions and 22 deletions
.vscode/launch.json
@@ -0,0 +1,64 @@ | ||
1 | { | |
2 | // Use IntelliSense to learn about possible attributes. | |
3 | // Hover to view descriptions of existing attributes. | |
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | |
5 | "version": "0.2.0", | |
6 | "configurations": [ | |
7 | { | |
8 | "type": "lldb", | |
9 | "request": "launch", | |
10 | "name": "Debug unit tests in library 'giterated-daemon'", | |
11 | "cargo": { | |
12 | "args": [ | |
13 | "test", | |
14 | "--no-run", | |
15 | "--lib", | |
16 | "--package=giterated-daemon" | |
17 | ], | |
18 | "filter": { | |
19 | "name": "giterated-daemon", | |
20 | "kind": "lib" | |
21 | } | |
22 | }, | |
23 | "args": [], | |
24 | "cwd": "${workspaceFolder}" | |
25 | }, | |
26 | { | |
27 | "type": "lldb", | |
28 | "request": "launch", | |
29 | "name": "Debug executable 'giterated-daemon'", | |
30 | "cargo": { | |
31 | "args": [ | |
32 | "build", | |
33 | "--bin=giterated-daemon", | |
34 | "--package=giterated-daemon" | |
35 | ], | |
36 | "filter": { | |
37 | "name": "giterated-daemon", | |
38 | "kind": "bin" | |
39 | } | |
40 | }, | |
41 | "args": [], | |
42 | "cwd": "${workspaceFolder}" | |
43 | }, | |
44 | { | |
45 | "type": "lldb", | |
46 | "request": "launch", | |
47 | "name": "Debug unit tests in executable 'giterated-daemon'", | |
48 | "cargo": { | |
49 | "args": [ | |
50 | "test", | |
51 | "--no-run", | |
52 | "--bin=giterated-daemon", | |
53 | "--package=giterated-daemon" | |
54 | ], | |
55 | "filter": { | |
56 | "name": "giterated-daemon", | |
57 | "kind": "bin" | |
58 | } | |
59 | }, | |
60 | "args": [], | |
61 | "cwd": "${workspaceFolder}" | |
62 | } | |
63 | ] | |
64 | } | |
64 | \ No newline at end of file |
Cargo.toml
@@ -16,7 +16,7 @@ tracing-subscriber = "0.3" | ||
16 | 16 | jsonwebtoken = { version = "*", features = ["use_pem"]} |
17 | 17 | log = "*" |
18 | 18 | rand = "*" |
19 | rsa = {version = "*", features = ["sha2"]} | |
19 | rsa = {version = "0.9", features = ["sha2"]} | |
20 | 20 | reqwest = "*" |
21 | 21 | |
22 | 22 | toml = { version = "0.7" } |
src/authentication.rs
@@ -1,12 +1,18 @@ | ||
1 | use std::{error::Error, time::SystemTime}; | |
1 | use std::{error::Error, os::raw, time::SystemTime}; | |
2 | 2 | |
3 | use jsonwebtoken::{encode, Algorithm, EncodingKey}; | |
3 | use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, TokenData, Validation}; | |
4 | 4 | use serde::{Deserialize, Serialize}; |
5 | 5 | use tokio::{fs::File, io::AsyncReadExt}; |
6 | 6 | use toml::Table; |
7 | 7 | |
8 | 8 | use crate::{ |
9 | messages::authentication::{AuthenticationTokenRequest, AuthenticationTokenResponse}, | |
9 | messages::{ | |
10 | authentication::{ | |
11 | AuthenticationTokenRequest, AuthenticationTokenResponse, TokenExtensionRequest, | |
12 | TokenExtensionResponse, | |
13 | }, | |
14 | InstanceAuthenticated, | |
15 | }, | |
10 | 16 | model::{instance::Instance, user::User}, |
11 | 17 | }; |
12 | 18 | |
@@ -72,4 +78,91 @@ impl AuthenticationTokenGranter { | ||
72 | 78 | |
73 | 79 | Ok(AuthenticationTokenResponse { token }) |
74 | 80 | } |
81 | ||
82 | pub async fn extension_request( | |
83 | &mut self, | |
84 | raw_request: InstanceAuthenticated<TokenExtensionRequest>, | |
85 | ) -> Result<TokenExtensionResponse, Box<dyn Error + Send>> { | |
86 | let request = raw_request.inner().await; | |
87 | ||
88 | let server_public_key = { | |
89 | let mut file = File::open(self.config["keys"]["public"].as_str().unwrap()) | |
90 | .await | |
91 | .unwrap(); | |
92 | ||
93 | let mut key = vec![]; | |
94 | file.read_to_end(&mut key).await.unwrap(); | |
95 | ||
96 | key | |
97 | }; | |
98 | ||
99 | let verification_key = DecodingKey::from_rsa_pem(&server_public_key).unwrap(); | |
100 | ||
101 | let data: TokenData<UserTokenMetadata> = decode( | |
102 | &request.token, | |
103 | &verification_key, | |
104 | &Validation::new(Algorithm::RS256), | |
105 | ) | |
106 | .unwrap(); | |
107 | ||
108 | info!("Token Extension Request Token validated"); | |
109 | ||
110 | let secret_key = self.config["authentication"]["secret_key"] | |
111 | .as_str() | |
112 | .unwrap(); | |
113 | ||
114 | if request.secret_key != secret_key { | |
115 | error!("Incorrect secret key!"); | |
116 | ||
117 | panic!() | |
118 | } | |
119 | ||
120 | let requester_public_key = public_key(&data.claims.generated_for).await.unwrap(); | |
121 | ||
122 | // Validate request | |
123 | raw_request.validate(requester_public_key).await.unwrap(); | |
124 | info!("Validated request for key extension"); | |
125 | ||
126 | let private_key = { | |
127 | let mut file = File::open(self.config["keys"]["private"].as_str().unwrap()) | |
128 | .await | |
129 | .unwrap(); | |
130 | ||
131 | let mut key = vec![]; | |
132 | file.read_to_end(&mut key).await.unwrap(); | |
133 | ||
134 | key | |
135 | }; | |
136 | ||
137 | let encoding_key = EncodingKey::from_rsa_pem(&private_key).unwrap(); | |
138 | ||
139 | let claims = UserTokenMetadata { | |
140 | // TODO: Probably exploitable | |
141 | user: data.claims.user, | |
142 | generated_for: data.claims.generated_for, | |
143 | exp: (SystemTime::UNIX_EPOCH.elapsed().unwrap() | |
144 | + std::time::Duration::from_secs(24 * 60 * 60)) | |
145 | .as_secs(), | |
146 | }; | |
147 | ||
148 | let token = encode( | |
149 | &jsonwebtoken::Header::new(Algorithm::RS256), | |
150 | &claims, | |
151 | &encoding_key, | |
152 | ) | |
153 | .unwrap(); | |
154 | ||
155 | Ok(TokenExtensionResponse { | |
156 | new_token: Some(token), | |
157 | }) | |
158 | } | |
159 | } | |
160 | ||
161 | async fn public_key(instance: &Instance) -> Result<String, Box<dyn Error>> { | |
162 | let key = reqwest::get(format!("https://{}/.giterated/pubkey.pem", instance.url)) | |
163 | .await? | |
164 | .text() | |
165 | .await?; | |
166 | ||
167 | Ok(key) | |
75 | 168 | } |
src/backend/git.rs
@@ -5,7 +5,7 @@ use std::error::Error; | ||
5 | 5 | use std::path::{Path, PathBuf}; |
6 | 6 | use thiserror::Error; |
7 | 7 | |
8 | use crate::messages::Authenticated; | |
8 | use crate::messages::UserAuthenticated; | |
9 | 9 | use crate::model::instance::Instance; |
10 | 10 | use crate::model::repository::{ |
11 | 11 | Commit, RepositoryObjectType, RepositoryTreeEntry, RepositoryVisibility, |
@@ -214,7 +214,7 @@ impl GitBackend { | ||
214 | 214 | impl RepositoryBackend for GitBackend { |
215 | 215 | async fn create_repository( |
216 | 216 | &mut self, |
217 | raw_request: &Authenticated<CreateRepositoryRequest>, | |
217 | raw_request: &UserAuthenticated<CreateRepositoryRequest>, | |
218 | 218 | ) -> Result<CreateRepositoryResponse, Box<dyn Error + Send>> { |
219 | 219 | let request = raw_request.inner().await; |
220 | 220 | |
@@ -224,7 +224,14 @@ impl RepositoryBackend for GitBackend { | ||
224 | 224 | .await |
225 | 225 | .unwrap(); |
226 | 226 | |
227 | assert!(matches!(raw_request.validate(public_key).await, Ok(()))); | |
227 | match raw_request.validate(public_key).await { | |
228 | Ok(_) => info!("Request was validated"), | |
229 | Err(err) => { | |
230 | error!("Failed to validate request: {:?}", err); | |
231 | panic!(); | |
232 | } | |
233 | } | |
234 | ||
228 | 235 | info!("Request was valid!"); |
229 | 236 | |
230 | 237 | // Check if repository already exists in the database |
src/backend/mod.rs
@@ -16,7 +16,7 @@ use crate::{ | ||
16 | 16 | UserBioRequest, UserBioResponse, UserDisplayImageRequest, UserDisplayImageResponse, |
17 | 17 | UserDisplayNameRequest, UserDisplayNameResponse, |
18 | 18 | }, |
19 | Authenticated, | |
19 | UserAuthenticated, | |
20 | 20 | }, |
21 | 21 | model::repository::RepositoryView, |
22 | 22 | }; |
@@ -25,7 +25,7 @@ use crate::{ | ||
25 | 25 | pub trait RepositoryBackend: IssuesBackend { |
26 | 26 | async fn create_repository( |
27 | 27 | &mut self, |
28 | request: &Authenticated<CreateRepositoryRequest>, | |
28 | request: &UserAuthenticated<CreateRepositoryRequest>, | |
29 | 29 | ) -> Result<CreateRepositoryResponse, Box<dyn Error + Send>>; |
30 | 30 | async fn repository_info( |
31 | 31 | &mut self, |
src/connection.rs
@@ -17,7 +17,7 @@ use crate::{ | ||
17 | 17 | handshake::{HandshakeFinalize, HandshakeMessage, HandshakeResponse}, |
18 | 18 | listener::Listeners, |
19 | 19 | messages::{ |
20 | authentication::{AuthenticationMessage, AuthenticationRequest}, | |
20 | authentication::{AuthenticationMessage, AuthenticationRequest, TokenExtensionResponse}, | |
21 | 21 | repository::{ |
22 | 22 | RepositoryMessage, RepositoryMessageKind, RepositoryRequest, RepositoryResponse, |
23 | 23 | }, |
@@ -61,7 +61,7 @@ pub async fn connection_worker( | ||
61 | 61 | ) { |
62 | 62 | let mut handshaked = false; |
63 | 63 | let this_instance = Instance { |
64 | url: String::from("127.0.0.1:8080"), | |
64 | url: String::from("giterated.dev"), | |
65 | 65 | }; |
66 | 66 | |
67 | 67 | while let Some(message) = socket.next().await { |
@@ -389,6 +389,26 @@ pub async fn connection_worker( | ||
389 | 389 | .unwrap(); |
390 | 390 | continue; |
391 | 391 | } |
392 | AuthenticationRequest::TokenExtension(request) => { | |
393 | let mut granter = auth_granter.lock().await; | |
394 | ||
395 | let response = granter | |
396 | .extension_request(request.clone()) | |
397 | .await | |
398 | .unwrap_or_else(|_| TokenExtensionResponse { new_token: None }); | |
399 | drop(granter); | |
400 | ||
401 | socket | |
402 | .send(Message::Binary( | |
403 | serde_json::to_vec(&MessageKind::Authentication( | |
404 | AuthenticationMessage::Response(crate::messages::authentication::AuthenticationResponse::TokenExtension(response)) | |
405 | )) | |
406 | .unwrap(), | |
407 | )) | |
408 | .await | |
409 | .unwrap(); | |
410 | continue; | |
411 | } | |
392 | 412 | }, |
393 | 413 | AuthenticationMessage::Response(_) => unreachable!(), |
394 | 414 | } |
src/main.rs
@@ -40,6 +40,9 @@ async fn main() -> Result<(), Box<dyn Error>> { | ||
40 | 40 | .password(config["postgres"]["password"].as_str().unwrap()) |
41 | 41 | .log_statements(log::LevelFilter::Off); |
42 | 42 | let db_pool = PgPool::connect_with(db_conn_options).await?; |
43 | ||
44 | debug!("Running database migrations..."); | |
45 | sqlx::migrate!().run(&db_pool).await?; | |
43 | 46 | info!("Connected"); |
44 | 47 | |
45 | 48 | let repository_backend: Arc<Mutex<dyn RepositoryBackend + Send>> = Arc::new(Mutex::new({ |
src/messages/authentication.rs
@@ -1,5 +1,7 @@ | ||
1 | 1 | use serde::{Deserialize, Serialize}; |
2 | 2 | |
3 | use super::InstanceAuthenticated; | |
4 | ||
3 | 5 | #[derive(Clone, Serialize, Deserialize)] |
4 | 6 | pub enum AuthenticationMessage { |
5 | 7 | Request(AuthenticationRequest), |
@@ -9,11 +11,13 @@ pub enum AuthenticationMessage { | ||
9 | 11 | #[derive(Clone, Serialize, Deserialize)] |
10 | 12 | pub enum AuthenticationRequest { |
11 | 13 | AuthenticationToken(AuthenticationTokenRequest), |
14 | TokenExtension(InstanceAuthenticated<TokenExtensionRequest>), | |
12 | 15 | } |
13 | 16 | |
14 | 17 | #[derive(Clone, Serialize, Deserialize)] |
15 | 18 | pub enum AuthenticationResponse { |
16 | 19 | AuthenticationToken(AuthenticationTokenResponse), |
20 | TokenExtension(TokenExtensionResponse), | |
17 | 21 | } |
18 | 22 | |
19 | 23 | #[derive(Clone, Serialize, Deserialize)] |
@@ -27,3 +31,14 @@ pub struct AuthenticationTokenRequest { | ||
27 | 31 | pub struct AuthenticationTokenResponse { |
28 | 32 | pub token: String, |
29 | 33 | } |
34 | ||
35 | #[derive(Clone, Serialize, Deserialize)] | |
36 | pub struct TokenExtensionRequest { | |
37 | pub secret_key: String, | |
38 | pub token: String, | |
39 | } | |
40 | ||
41 | #[derive(Clone, Serialize, Deserialize)] | |
42 | pub struct TokenExtensionResponse { | |
43 | pub new_token: Option<String>, | |
44 | } |
src/messages/mod.rs
@@ -1,15 +1,16 @@ | ||
1 | 1 | use std::{error::Error, fmt::Debug}; |
2 | 2 | |
3 | 3 | use rsa::{ |
4 | pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, | |
4 | 5 | pkcs8::{DecodePrivateKey, DecodePublicKey}, |
5 | 6 | pss::{Signature, SigningKey, VerifyingKey}, |
6 | 7 | sha2::Sha256, |
7 | 8 | signature::{RandomizedSigner, SignatureEncoding, Verifier}, |
8 | RsaPrivateKey, RsaPublicKey, pkcs1::DecodeRsaPrivateKey, | |
9 | RsaPrivateKey, RsaPublicKey, | |
9 | 10 | }; |
10 | 11 | use serde::{Deserialize, Serialize}; |
11 | 12 | |
12 | use crate::handshake::HandshakeMessage; | |
13 | use crate::{handshake::HandshakeMessage, model::instance::Instance}; | |
13 | 14 | |
14 | 15 | use self::{authentication::AuthenticationMessage, repository::RepositoryMessage}; |
15 | 16 | |
@@ -25,19 +26,98 @@ pub enum MessageKind { | ||
25 | 26 | Authentication(AuthenticationMessage), |
26 | 27 | } |
27 | 28 | |
29 | /// An authenticated message, where the instance is authenticating | |
30 | /// a request it is making for itself. | |
31 | #[derive(Serialize, Deserialize)] | |
32 | pub struct InstanceAuthenticated<T: Serialize> { | |
33 | message: T, | |
34 | instance: Instance, | |
35 | signature: Vec<u8>, | |
36 | } | |
37 | ||
38 | impl<T> Clone for InstanceAuthenticated<T> | |
39 | where | |
40 | T: Clone + Serialize, | |
41 | { | |
42 | fn clone(&self) -> Self { | |
43 | Self { | |
44 | message: self.message.clone(), | |
45 | instance: self.instance.clone(), | |
46 | signature: self.signature.clone(), | |
47 | } | |
48 | } | |
49 | } | |
50 | ||
51 | impl<T> Debug for InstanceAuthenticated<T> | |
52 | where | |
53 | T: Debug + Serialize, | |
54 | { | |
55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
56 | f.debug_struct("Authenticated") | |
57 | .field("message", &self.message) | |
58 | .field("instance", &self.instance) | |
59 | .field("signature", &self.signature) | |
60 | .finish() | |
61 | } | |
62 | } | |
63 | ||
64 | impl<T: Serialize> InstanceAuthenticated<T> { | |
65 | pub fn new( | |
66 | message: T, | |
67 | instance: Instance, | |
68 | private_key: String, | |
69 | ) -> Result<Self, Box<dyn Error>> { | |
70 | let mut rng = rand::thread_rng(); | |
71 | ||
72 | let private_key = RsaPrivateKey::from_pkcs1_pem(&private_key)?; | |
73 | let signing_key = SigningKey::<Sha256>::new(private_key); | |
74 | ||
75 | let message_json = serde_json::to_vec(&message)?; | |
76 | ||
77 | let signature = signing_key.sign_with_rng(&mut rng, &message_json); | |
78 | ||
79 | Ok(Self { | |
80 | message, | |
81 | instance, | |
82 | signature: signature.to_vec(), | |
83 | }) | |
84 | } | |
85 | ||
86 | pub async fn inner(&self) -> &T { | |
87 | &self.message | |
88 | } | |
89 | ||
90 | pub async fn validate(&self, key: String) -> Result<(), Box<dyn Error>> { | |
91 | let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); | |
92 | ||
93 | let verifying_key: VerifyingKey<Sha256> = VerifyingKey::new(public_key); | |
94 | ||
95 | let message_json = serde_json::to_vec(&self.message).unwrap(); | |
96 | ||
97 | verifying_key | |
98 | .verify( | |
99 | &message_json, | |
100 | &Signature::try_from(self.signature.as_ref()).unwrap(), | |
101 | ) | |
102 | .unwrap(); | |
103 | ||
104 | Ok(()) | |
105 | } | |
106 | } | |
107 | ||
28 | 108 | /// An authenticated message. |
29 | 109 | /// |
30 | 110 | /// Includes the message, with a digest generated with |
31 | 111 | /// our private key. |
32 | 112 | #[derive(Serialize, Deserialize)] |
33 | pub struct Authenticated<T: Serialize> { | |
113 | pub struct UserAuthenticated<T: Serialize> { | |
34 | 114 | #[serde(flatten)] |
35 | 115 | message: T, |
36 | 116 | token: String, |
37 | 117 | digest: Vec<u8>, |
38 | 118 | } |
39 | 119 | |
40 | impl<T> Clone for Authenticated<T> | |
120 | impl<T> Clone for UserAuthenticated<T> | |
41 | 121 | where |
42 | 122 | T: Clone + Serialize, |
43 | 123 | { |
@@ -50,7 +130,7 @@ where | ||
50 | 130 | } |
51 | 131 | } |
52 | 132 | |
53 | impl<T> Debug for Authenticated<T> | |
133 | impl<T> Debug for UserAuthenticated<T> | |
54 | 134 | where |
55 | 135 | T: Debug + Serialize, |
56 | 136 | { |
@@ -63,7 +143,7 @@ where | ||
63 | 143 | } |
64 | 144 | } |
65 | 145 | |
66 | impl<T: Serialize> Authenticated<T> { | |
146 | impl<T: Serialize> UserAuthenticated<T> { | |
67 | 147 | pub fn new(message: T, token: String, private_key: String) -> Result<Self, Box<dyn Error>> { |
68 | 148 | let mut rng = rand::thread_rng(); |
69 | 149 | |
@@ -86,13 +166,18 @@ impl<T: Serialize> Authenticated<T> { | ||
86 | 166 | } |
87 | 167 | |
88 | 168 | pub async fn validate(&self, key: String) -> Result<(), Box<dyn Error>> { |
89 | let public_key = RsaPublicKey::from_public_key_pem(&key)?; | |
169 | let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); | |
90 | 170 | |
91 | 171 | let verifying_key: VerifyingKey<Sha256> = VerifyingKey::new(public_key); |
92 | 172 | |
93 | let message_json = serde_json::to_vec(&self.message)?; | |
173 | let message_json = serde_json::to_vec(&self.message).unwrap(); | |
94 | 174 | |
95 | verifying_key.verify(&message_json, &Signature::try_from(self.digest.as_ref())?)?; | |
175 | verifying_key | |
176 | .verify( | |
177 | &message_json, | |
178 | &Signature::try_from(self.digest.as_ref()).unwrap(), | |
179 | ) | |
180 | .unwrap(); | |
96 | 181 | |
97 | 182 | Ok(()) |
98 | 183 | } |
src/messages/repository.rs
@@ -6,7 +6,7 @@ use crate::model::{ | ||
6 | 6 | user::User, |
7 | 7 | }; |
8 | 8 | |
9 | use super::Authenticated; | |
9 | use super::UserAuthenticated; | |
10 | 10 | |
11 | 11 | #[derive(Clone, Serialize, Deserialize)] |
12 | 12 | pub struct RepositoryMessage { |
@@ -22,7 +22,7 @@ pub enum RepositoryMessageKind { | ||
22 | 22 | |
23 | 23 | #[derive(Clone, Serialize, Deserialize)] |
24 | 24 | pub enum RepositoryRequest { |
25 | CreateRepository(Authenticated<CreateRepositoryRequest>), | |
25 | CreateRepository(UserAuthenticated<CreateRepositoryRequest>), | |
26 | 26 | RepositoryFileInspect(RepositoryFileInspectRequest), |
27 | 27 | RepositoryInfo(RepositoryInfoRequest), |
28 | 28 | IssuesCount(RepositoryIssuesCountRequest), |