/* * Copyright (c) 2025 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using Newtonsoft.Json; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Common.Legacy.Vpn; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Crypto.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.DisconnectLogs; namespace ProtonVPN.Vpn.ServerValidation; public class ServerValidator : IServerValidator { private const int SERVER_PUBLIC_KEY_LENGTH = 44; private readonly ILogger _logger; private readonly IConfiguration _config; private readonly IEd25519SignatureValidator _ed25519SignatureValidator; public ServerValidator(ILogger logger, IConfiguration config, IEd25519SignatureValidator ed25519SignatureValidator) { _logger = logger; _config = config; _ed25519SignatureValidator = ed25519SignatureValidator; } public VpnError Validate(VpnHost server) { VpnError error; try { error = ValidatePublicKey(server); if (error == VpnError.None) { error = ValidateSignature(server); } } catch (Exception ex) { _logger.Error(ex.Message); return VpnError.Unknown; } return error; } private VpnError ValidatePublicKey(VpnHost host) { if (host.X25519PublicKey is null) { return VpnError.None; } if (string.IsNullOrWhiteSpace(host.X25519PublicKey.Base64)) { _logger.Error($"The server with name '{host.Name}' and IP '{host.Ip}' " + $"has an empty public key."); return VpnError.ServerValidationError; } if (host.X25519PublicKey.Base64.Length != SERVER_PUBLIC_KEY_LENGTH) { _logger.Error($"Incorrect server public key length " + $"{host.X25519PublicKey.Base64.Length} when it should be {SERVER_PUBLIC_KEY_LENGTH}, " + $"for the server with name '{host.Name}' and IP '{host.Ip}'."); return VpnError.ServerValidationError; } if (!host.X25519PublicKey.Base64.IsValidBase64Key()) { _logger.Error($"The server with name '{host.Name}' and IP '{host.Ip}' " + $"has a public key that is not base64: {host.X25519PublicKey.Base64}"); return VpnError.ServerValidationError; } return VpnError.None; } private VpnError ValidateSignature(VpnHost host) { if (string.IsNullOrWhiteSpace(host.Signature)) { _logger.Error($"The server with name '{host.Name}' and IP '{host.Ip}' " + $"is missing its signature."); return VpnError.ServerValidationError; } string base64publicKey = _config.ServerValidationPublicKey; if (string.IsNullOrWhiteSpace(base64publicKey)) { _logger.Error($"The {nameof(_config.ServerValidationPublicKey)} " + $"configuration is empty and it is therefore impossible to validate servers."); return VpnError.NoServerValidationPublicKey; } string serverValidationData = GetServerValidationData(host); if (!_ed25519SignatureValidator.IsValid(serverValidationData, host.Signature, base64publicKey)) { _logger.Error($"The server with name '{host.Name}' and IP '{host.Ip}' " + $"has an incorrect signature '{host.Signature}'."); return VpnError.ServerValidationError; } return VpnError.None; } private string GetServerValidationData(VpnHost host) { try { ServerValidationObject server = new() { Server = new PhysicalServerValidationObject { EntryIP = host.Ip, Label = host.Label } }; return JsonConvert.SerializeObject(server); } catch (Exception) { throw new Exception($"Failed to generate the JSON validation object " + $"for the server with name '{host.Name}' and IP '{host.Ip}'."); } } }