Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/defguard_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ x25519-dalek = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
bytes = { workspace = true }
ed25519-dalek = "2.2.0"
ed25519-dalek = { version = "2.2.0", features = ["rand_core"] }

# https://github.com/juhaku/utoipa/issues/1345
[dependencies.zip]
Expand Down
99 changes: 80 additions & 19 deletions crates/defguard_core/src/db/models/biometric_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::{
db::{Id, NoId},
random::gen_alphanumeric,
};
use base64::Engine;
use base64::engine::general_purpose;
use base64::{Engine, prelude::BASE64_STANDARD};
use ed25519_dalek::Verifier;
use ed25519_dalek::{Signature, VerifyingKey};
use model_derive::Model;
Expand All @@ -18,6 +18,16 @@ pub enum BiometricAuthError {
InvalidSignature,
#[error("Verification of submitted challenge failed. {0}")]
ChallengeFailed(String),
#[error("Base64 decoding failed. {0}")]
Base64DecodeError(#[from] base64::DecodeError),
#[error("Challenge had no owner")]
ChallengeNotOwned,
}

impl From<BiometricAuthError> for tonic::Status {
fn from(value: BiometricAuthError) -> Self {
Self::invalid_argument(value.to_string())
}
}

#[derive(Model, Clone)]
Expand All @@ -36,6 +46,13 @@ impl BiometricAuth {
pub_key,
}
}
pub fn validate_pubkey(pub_key: &str) -> Result<(), BiometricAuthError> {
let decoded = BASE64_STANDARD.decode(pub_key)?;
if decoded.len() != ed25519_dalek::PUBLIC_KEY_LENGTH {
return Err(BiometricAuthError::InvalidPublicKey);
}
Ok(())
}
}

impl BiometricAuth<Id> {
Expand Down Expand Up @@ -78,7 +95,7 @@ pub struct BiometricChallenge {
}

fn decode_pub_key(public_key: &str) -> Result<VerifyingKey, BiometricAuthError> {
let pub_bytes: [u8; 32] = general_purpose::STANDARD
let pub_bytes: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = general_purpose::STANDARD
.decode(public_key)
.map_err(|_| BiometricAuthError::InvalidPublicKey)?
.try_into()
Expand All @@ -100,35 +117,79 @@ impl BiometricChallenge {
})
}

#[must_use]
pub fn verify(&self, signed_challenge: &str) -> bool {
pub fn verify(&self, signed_challenge: &str) -> Result<(), BiometricAuthError> {
if let Some(auth_pub_key) = &self.auth_pub_key {
return match verify(signed_challenge, auth_pub_key.as_str(), &self.challenge) {
Ok(res) => res,
Err(e) => {
error!("Biometric auth verification failed!\n Reason: {e}");
false
}
};
return verify(signed_challenge, auth_pub_key.as_str(), &self.challenge);
}
false
Err(BiometricAuthError::ChallengeNotOwned)
}
}

fn verify(
signed_challenge: &str,
signature: &str,
public_key: &str,
original_challenge: &str,
) -> Result<bool, BiometricAuthError> {
) -> Result<(), BiometricAuthError> {
let verifying_key = decode_pub_key(public_key)?;
let sig_bytes: [u8; 64] = general_purpose::STANDARD
.decode(signed_challenge)
let sig_bytes: [u8; ed25519_dalek::SIGNATURE_LENGTH] = general_purpose::STANDARD
.decode(signature)
.map_err(|_| BiometricAuthError::InvalidSignature)?
.try_into()
.map_err(|_| BiometricAuthError::InvalidSignature)?;
let signature = Signature::from_bytes(&sig_bytes);
match verifying_key.verify(original_challenge.as_bytes(), &signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false),
verifying_key
.verify(original_challenge.as_bytes(), &signature)
.map_err(|_| BiometricAuthError::InvalidSignature)
}

#[cfg(test)]
mod test {
use super::*;
use base64::engine::general_purpose;
use ed25519_dalek::Signer;
use matches::assert_matches;

#[test]
fn test_verify_valid_sig() {
let mut csprng = rand_core::OsRng;
let signing_key = ed25519_dalek::SigningKey::generate(&mut csprng);
let challenge = "test-challenge";
let signed = signing_key.sign(challenge.as_bytes());
let serialized_signature = BASE64_STANDARD.encode(signed.to_bytes());
let serialized_pub_key = BASE64_STANDARD.encode(signing_key.verifying_key().as_bytes());

assert_matches!(
verify(&serialized_signature, &serialized_pub_key, challenge),
Ok(())
);
}

#[test]
fn test_verify_invalid_signature() {
let mut csprng = rand_core::OsRng;
let signing_key = ed25519_dalek::SigningKey::generate(&mut csprng);
let challenge = "test-challenge";

let bad_signature = [0u8; ed25519_dalek::SIGNATURE_LENGTH];
let signature_b64 = general_purpose::STANDARD.encode(bad_signature);
let public_key_b64 =
general_purpose::STANDARD.encode(signing_key.verifying_key().as_bytes());

let result = verify(&signature_b64, &public_key_b64, challenge);

assert_matches!(result, Err(BiometricAuthError::InvalidSignature));
}

#[test]
fn test_verify_invalid_public_key() {
let challenge = "test-challenge";
let signature = [0u8; ed25519_dalek::SIGNATURE_LENGTH];
let signature_b64 = general_purpose::STANDARD.encode(signature);

let bad_pub_key = general_purpose::STANDARD.encode([1, 2, 3]);

let result = verify(&signature_b64, &bad_pub_key, challenge);

assert_matches!(result, Err(BiometricAuthError::InvalidPublicKey));
}
}
8 changes: 6 additions & 2 deletions crates/defguard_core/src/grpc/client_mfa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,15 @@ impl ClientMfaServer {
if let Some(signed_challenge) = request.code {
match challenge.verify(signed_challenge.as_str()) {
// verification passed
true => {
Ok(()) => {
debug!("Signed challenge verified successfully.");
}
// challenge rejected
false => {
Err(e) => {
error!(
"Verification of challenge for device {0} failed ! Reason {e}",
&device.name
);
self.emit_event(BidiStreamEvent {
context,
event: BidiStreamEventType::DesktopClientMfa(Box::new(
Expand Down
1 change: 1 addition & 0 deletions crates/defguard_core/src/grpc/enrollment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ impl EnrollmentServer {
));
}
};
BiometricAuth::validate_pubkey(&request.device_pub_key)?;
let mobile_auth = BiometricAuth::new(device.id, request.auth_pub_key);
let _ = mobile_auth.save(&self.pool).await.map_err(|err| {
error!("Failed to save mobile auth into db : {err}");
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
"standard-version": "^9.5.0",
"type-fest": "^4.41.0",
"typescript": "~5.9.2",
"vite": "^7.0.6",
"vite": "^7.1.0",
"vite-plugin-package-version": "^1.1.0"
}
}
40 changes: 20 additions & 20 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions web/src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4774,6 +4774,9 @@ type RootTranslation = {
* B​a​s​e​d​ ​o​n​ ​t​h​i​s​ ​a​d​d​r​e​s​s​ ​V​P​N​ ​n​e​t​w​o​r​k​ ​a​d​d​r​e​s​s​ ​w​i​l​l​ ​b​e​ ​d​e​f​i​n​e​d​,​ ​e​g​.​ ​1​0​.​1​0​.​1​0​.​1​/​2​4​ ​(​a​n​d​ ​V​P​N​ ​n​e​t​w​o​r​k​ ​w​i​l​l​ ​b​e​:​ ​1​0​.​1​0​.​1​0​.​0​/​2​4​)​.​ ​Y​o​u​ ​c​a​n​ ​o​p​t​i​o​n​a​l​l​y​ ​s​p​e​c​i​f​y​ ​m​u​l​t​i​p​l​e​ ​a​d​d​r​e​s​s​e​s​ ​s​e​p​a​r​a​t​e​d​ ​b​y​ ​a​ ​c​o​m​m​a​.​ ​T​h​e​ ​f​i​r​s​t​ ​a​d​d​r​e​s​s​ ​i​s​ ​t​h​e​ ​p​r​i​m​a​r​y​ ​a​d​d​r​e​s​s​,​ ​a​n​d​ ​t​h​i​s​ ​o​n​e​ ​w​i​l​l​ ​b​e​ ​u​s​e​d​ ​f​o​r​ ​I​P​ ​a​d​d​r​e​s​s​ ​a​s​s​i​g​n​m​e​n​t​ ​f​o​r​ ​d​e​v​i​c​e​s​.​ ​T​h​e​ ​o​t​h​e​r​ ​I​P​ ​a​d​d​r​e​s​s​e​s​ ​a​r​e​ ​a​u​x​i​l​i​a​r​y​ ​a​n​d​ ​a​r​e​ ​n​o​t​ ​m​a​n​a​g​e​d​ ​b​y​ ​D​e​f​g​u​a​r​d​.
*/
address: string
/**
* P​u​b​l​i​c​ ​I​P​ ​a​d​d​r​e​s​s​ ​o​r​ ​d​o​m​a​i​n​ ​n​a​m​e​ ​t​o​ ​w​h​i​c​h​ ​t​h​e​ ​r​e​m​o​t​e​ ​p​e​e​r​s​/​u​s​e​r​s​ ​w​i​l​l​ ​c​o​n​n​e​c​t​ ​t​o​.​ ​T​h​i​s​ ​a​d​d​r​e​s​s​ ​w​i​l​l​ ​b​e​ ​u​s​e​d​ ​i​n​ ​t​h​e​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​f​o​r​ ​t​h​e​ ​c​l​i​e​n​t​s​,​ ​b​u​t​ ​D​e​f​g​u​a​r​d​ ​G​a​t​e​w​a​y​s​ ​d​o​ ​n​o​t​ ​b​i​n​d​ ​t​o​ ​t​h​i​s​ ​a​d​d​r​e​s​s​.
*/
endpoint: string
/**
* G​a​t​e​w​a​y​ ​p​u​b​l​i​c​ ​a​d​d​r​e​s​s​,​ ​u​s​e​d​ ​b​y​ ​V​P​N​ ​u​s​e​r​s​ ​t​o​ ​c​o​n​n​e​c​t
Expand Down Expand Up @@ -11370,6 +11373,9 @@ export type TranslationFunctions = {
* Based on this address VPN network address will be defined, eg. 10.10.10.1/24 (and VPN network will be: 10.10.10.0/24). You can optionally specify multiple addresses separated by a comma. The first address is the primary address, and this one will be used for IP address assignment for devices. The other IP addresses are auxiliary and are not managed by Defguard.
*/
address: () => LocalizedString
/**
* Public IP address or domain name to which the remote peers/users will connect to. This address will be used in the configuration for the clients, but Defguard Gateways do not bind to this address.
*/
endpoint: () => LocalizedString
/**
* Gateway public address, used by VPN users to connect
Expand Down