Skip to content

Instantly share code, notes, and snippets.

@celsowm
Last active January 27, 2026 21:31
Show Gist options
  • Select an option

  • Save celsowm/7fde416cca88cfcd0e549d68c4dbffd1 to your computer and use it in GitHub Desktop.

Select an option

Save celsowm/7fde416cca88cfcd0e549d68c4dbffd1 to your computer and use it in GitHub Desktop.
projeto assinador open a3

Cross-Platform Desktop PDF Signing Application (.NET 8 + Avalonia)

Complete cross-platform solution supporting Windows and macOS for PDF digital signatures.

Project Structure

DigitalSignerApp/
├── DigitalSignerApp.sln
├── DigitalSignerApp/
│   ├── DigitalSignerApp.csproj
│   ├── Program.cs
│   ├── App.axaml
│   ├── App.axaml.cs
│   ├── ViewModels/
│   │   ├── ViewModelBase.cs
│   │   ├── MainWindowViewModel.cs
│   │   └── CertificateSelectionViewModel.cs
│   ├── Views/
│   │   ├── MainWindow.axaml
│   │   ├── MainWindow.axaml.cs
│   │   ├── CertificateSelectionDialog.axaml
│   │   └── CertificateSelectionDialog.axaml.cs
│   ├── Services/
│   │   ├── Interfaces/
│   │   │   ├── ICertificateService.cs
│   │   │   ├── ISignerService.cs
│   │   │   └── IWebSocketServer.cs
│   │   ├── WebSocketServer.cs
│   │   ├── CertificateService.cs
│   │   ├── WindowsCertificateService.cs
│   │   ├── MacOSCertificateService.cs
│   │   ├── PAdESSignerService.cs
│   │   └── CAdESSignerService.cs
│   ├── Models/
│   │   ├── SignRequest.cs
│   │   ├── SignResponse.cs
│   │   └── AppSettings.cs
│   └── Utils/
│       ├── CertificateUtils.cs
│       ├── PlatformHelper.cs
│       └── SingleInstanceManager.cs
├── DigitalSignerApp.Desktop/
│   ├── DigitalSignerApp.Desktop.csproj
│   └── Program.cs
└── assets/
    ├── app.ico
    └── app.icns

1. Solution File (DigitalSignerApp.sln)

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalSignerApp", "DigitalSignerApp\DigitalSignerApp.csproj", "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalSignerApp.Desktop", "DigitalSignerApp.Desktop\DigitalSignerApp.Desktop.csproj", "{YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}"
EndProject
Global
    GlobalSection(SolutionConfigurationPlatforms) = preSolution
        Debug|Any CPU = Debug|Any CPU
        Release|Any CPU = Release|Any CPU
    EndGlobalSection
    GlobalSection(ProjectConfigurationPlatforms) = postSolution
        {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}.Release|Any CPU.Build.0 = Release|Any CPU
        {YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
        {YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}.Debug|Any CPU.Build.0 = Debug|Any CPU
        {YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}.Release|Any CPU.ActiveCfg = Release|Any CPU
        {YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}.Release|Any CPU.Build.0 = Release|Any CPU
    EndGlobalSection
EndGlobal

2. Main Project File (DigitalSignerApp/DigitalSignerApp.csproj)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>latest</LangVersion>
    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
  </PropertyGroup>

  <ItemGroup>
    <!-- Avalonia UI -->
    <PackageReference Include="Avalonia" Version="11.0.6" />
    <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
    <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
    <PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6" />
    
    <!-- iText7 for PAdES signing -->
    <PackageReference Include="itext7" Version="8.0.2" />
    <PackageReference Include="itext7.bouncy-castle-adapter" Version="8.0.2" />
    
    <!-- BouncyCastle for CAdES -->
    <PackageReference Include="BouncyCastle.Cryptography" Version="2.2.1" />
    
    <!-- WebSocket Server -->
    <PackageReference Include="Fleck" Version="1.2.0" />
    
    <!-- JSON Serialization (using System.Text.Json instead of Newtonsoft) -->
    <PackageReference Include="System.Text.Json" Version="8.0.0" />
    
    <!-- Logging -->
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
    <PackageReference Include="Serilog" Version="3.1.1" />
    <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
    <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
    
    <!-- Dependency Injection -->
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
    
    <!-- CommunityToolkit for MVVM -->
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
  </ItemGroup>

  <ItemGroup>
    <!-- Compile XAML files -->
    <AvaloniaResource Include="Assets\**" />
  </ItemGroup>

</Project>

3. Desktop Entry Point (DigitalSignerApp.Desktop/DigitalSignerApp.Desktop.csproj)

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <ApplicationIcon>..\assets\app.ico</ApplicationIcon>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    
    <!-- macOS specific -->
    <CFBundleName>DigitalSignerApp</CFBundleName>
    <CFBundleDisplayName>Digital Signer App</CFBundleDisplayName>
    <CFBundleIdentifier>com.yourcompany.digitalsigner</CFBundleIdentifier>
    <CFBundleVersion>1.0.0</CFBundleVersion>
    <CFBundleShortVersionString>1.0.0</CFBundleShortVersionString>
    <CFBundleIconFile>app.icns</CFBundleIconFile>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\DigitalSignerApp\DigitalSignerApp.csproj" />
  </ItemGroup>

  <!-- Windows manifest for single instance -->
  <ItemGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
    <None Update="app.manifest">
      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>

DigitalSignerApp.Desktop/Program.cs

using System;
using Avalonia;
using DigitalSignerApp;
using DigitalSignerApp.Utils;

namespace DigitalSignerApp.Desktop;

class Program
{
    [STAThread]
    public static void Main(string[] args)
    {
        // Check for single instance
        using var singleInstance = new SingleInstanceManager("DigitalSignerApp");
        
        if (!singleInstance.TryAcquire())
        {
            Console.WriteLine("Application is already running.");
            singleInstance.NotifyExistingInstance();
            return;
        }

        BuildAvaloniaApp()
            .StartWithClassicDesktopLifetime(args);
    }

    public static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>()
            .UsePlatformDetect()
            .WithInterFont()
            .LogToTrace();
}

DigitalSignerApp.Desktop/app.manifest (Windows only)

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="DigitalSignerApp"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="asInvoker" uiAccess="false"/>
      </requestedPrivileges>
    </security>
  </trustInfo>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
    </application>
  </compatibility>
</assembly>

4. Models

Models/SignRequest.cs

using System.Text.Json.Serialization;

namespace DigitalSignerApp.Models;

public class SignRequest
{
    [JsonPropertyName("requestId")]
    public string RequestId { get; set; } = Guid.NewGuid().ToString();

    [JsonPropertyName("action")]
    public string Action { get; set; } = string.Empty;

    [JsonPropertyName("pdfBase64")]
    public string? PdfBase64 { get; set; }

    [JsonPropertyName("dataBase64")]
    public string? DataBase64 { get; set; }

    [JsonPropertyName("signatureType")]
    public string SignatureType { get; set; } = "pades";

    [JsonPropertyName("reason")]
    public string? Reason { get; set; }

    [JsonPropertyName("location")]
    public string? Location { get; set; }

    [JsonPropertyName("certificateThumbprint")]
    public string? CertificateThumbprint { get; set; }

    [JsonPropertyName("visibleSignature")]
    public bool VisibleSignature { get; set; } = false;

    [JsonPropertyName("signaturePosition")]
    public SignaturePosition? SignaturePosition { get; set; }

    [JsonPropertyName("tsaUrl")]
    public string? TsaUrl { get; set; }

    [JsonPropertyName("includeTimestamp")]
    public bool IncludeTimestamp { get; set; } = false;
}

public class SignaturePosition
{
    [JsonPropertyName("page")]
    public int Page { get; set; } = 1;

    [JsonPropertyName("x")]
    public float X { get; set; }

    [JsonPropertyName("y")]
    public float Y { get; set; }

    [JsonPropertyName("width")]
    public float Width { get; set; } = 200;

    [JsonPropertyName("height")]
    public float Height { get; set; } = 60;
}

Models/SignResponse.cs

using System.Text.Json.Serialization;

namespace DigitalSignerApp.Models;

public class SignResponse
{
    [JsonPropertyName("requestId")]
    public string RequestId { get; set; } = string.Empty;

    [JsonPropertyName("success")]
    public bool Success { get; set; }

    [JsonPropertyName("errorCode")]
    public string? ErrorCode { get; set; }

    [JsonPropertyName("message")]
    public string? Message { get; set; }

    [JsonPropertyName("signedPdfBase64")]
    public string? SignedPdfBase64 { get; set; }

    [JsonPropertyName("signatureBase64")]
    public string? SignatureBase64 { get; set; }

    [JsonPropertyName("certificateInfo")]
    public CertificateInfo? CertificateInfo { get; set; }

    [JsonPropertyName("certificates")]
    public List<CertificateInfo>? Certificates { get; set; }

    [JsonPropertyName("signatureType")]
    public string? SignatureType { get; set; }

    [JsonPropertyName("timestamp")]
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;

    [JsonPropertyName("version")]
    public string Version { get; set; } = "1.0.0";

    public static SignResponse Error(string requestId, string message, string? errorCode = null)
    {
        return new SignResponse
        {
            RequestId = requestId,
            Success = false,
            Message = message,
            ErrorCode = errorCode
        };
    }

    public static SignResponse Ok(string requestId, string? message = null)
    {
        return new SignResponse
        {
            RequestId = requestId,
            Success = true,
            Message = message
        };
    }
}

public class CertificateInfo
{
    [JsonPropertyName("thumbprint")]
    public string Thumbprint { get; set; } = string.Empty;

    [JsonPropertyName("subject")]
    public string Subject { get; set; } = string.Empty;

    [JsonPropertyName("issuer")]
    public string Issuer { get; set; } = string.Empty;

    [JsonPropertyName("notBefore")]
    public DateTime NotBefore { get; set; }

    [JsonPropertyName("notAfter")]
    public DateTime NotAfter { get; set; }

    [JsonPropertyName("serialNumber")]
    public string SerialNumber { get; set; } = string.Empty;

    [JsonPropertyName("isValid")]
    public bool IsValid { get; set; }

    [JsonPropertyName("commonName")]
    public string CommonName { get; set; } = string.Empty;

    [JsonPropertyName("keyType")]
    public string KeyType { get; set; } = string.Empty;

    [JsonPropertyName("keySize")]
    public int KeySize { get; set; }

    [JsonPropertyName("isHardwareToken")]
    public bool IsHardwareToken { get; set; }
}

Models/AppSettings.cs

namespace DigitalSignerApp.Models;

public class AppSettings
{
    public int WebSocketPort { get; set; } = 9876;
    public bool AutoStart { get; set; } = true;
    public bool MinimizeToTray { get; set; } = true;
    public bool StartMinimized { get; set; } = false;
    public string? DefaultTsaUrl { get; set; }
    public bool EnableSecureWebSocket { get; set; } = false;
    public string? SslCertificatePath { get; set; }
    public string? SslCertificatePassword { get; set; }
    public LogLevel LogLevel { get; set; } = LogLevel.Information;
    public int MaxLogFileSizeMB { get; set; } = 10;
    public int MaxLogRetentionDays { get; set; } = 30;
}

public enum LogLevel
{
    Verbose,
    Debug,
    Information,
    Warning,
    Error,
    Fatal
}

5. Utilities

Utils/PlatformHelper.cs

using System.Runtime.InteropServices;

namespace DigitalSignerApp.Utils;

public static class PlatformHelper
{
    public static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
    public static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
    public static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

    public static string GetAppDataPath()
    {
        string basePath;

        if (IsWindows)
        {
            basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        }
        else if (IsMacOS)
        {
            basePath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                "Library", "Application Support");
        }
        else
        {
            basePath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                ".config");
        }

        var appPath = Path.Combine(basePath, "DigitalSignerApp");
        Directory.CreateDirectory(appPath);
        return appPath;
    }

    public static string GetLogPath()
    {
        var logPath = Path.Combine(GetAppDataPath(), "logs");
        Directory.CreateDirectory(logPath);
        return logPath;
    }

    public static string GetSettingsPath()
    {
        return Path.Combine(GetAppDataPath(), "settings.json");
    }
}

Utils/SingleInstanceManager.cs

using System.IO.Pipes;
using System.Runtime.InteropServices;

namespace DigitalSignerApp.Utils;

public class SingleInstanceManager : IDisposable
{
    private readonly string _appName;
    private Mutex? _mutex;
    private CancellationTokenSource? _pipeServerCts;
    
    public event EventHandler? OnSecondInstanceStarted;

    public SingleInstanceManager(string appName)
    {
        _appName = appName;
    }

    public bool TryAcquire()
    {
        var mutexName = GetMutexName();
        _mutex = new Mutex(true, mutexName, out bool createdNew);

        if (createdNew)
        {
            StartPipeServer();
            return true;
        }

        return false;
    }

    public void NotifyExistingInstance()
    {
        try
        {
            using var client = new NamedPipeClientStream(".", GetPipeName(), PipeDirection.Out);
            client.Connect(1000);
            using var writer = new StreamWriter(client);
            writer.WriteLine("ACTIVATE");
            writer.Flush();
        }
        catch
        {
            // Ignore errors when notifying
        }
    }

    private void StartPipeServer()
    {
        _pipeServerCts = new CancellationTokenSource();
        _ = Task.Run(async () =>
        {
            while (!_pipeServerCts.Token.IsCancellationRequested)
            {
                try
                {
                    using var server = new NamedPipeServerStream(
                        GetPipeName(), 
                        PipeDirection.In, 
                        1, 
                        PipeTransmissionMode.Byte, 
                        PipeOptions.Asynchronous);
                    
                    await server.WaitForConnectionAsync(_pipeServerCts.Token);
                    
                    using var reader = new StreamReader(server);
                    var message = await reader.ReadLineAsync();
                    
                    if (message == "ACTIVATE")
                    {
                        OnSecondInstanceStarted?.Invoke(this, EventArgs.Empty);
                    }
                }
                catch (OperationCanceledException)
                {
                    break;
                }
                catch
                {
                    await Task.Delay(100);
                }
            }
        });
    }

    private string GetMutexName()
    {
        return PlatformHelper.IsWindows 
            ? $"Global\\{_appName}_Mutex" 
            : $"{_appName}_Mutex";
    }

    private string GetPipeName()
    {
        return $"{_appName}_Pipe";
    }

    public void Dispose()
    {
        _pipeServerCts?.Cancel();
        _pipeServerCts?.Dispose();
        _mutex?.ReleaseMutex();
        _mutex?.Dispose();
    }
}

Utils/CertificateUtils.cs

using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using DigitalSignerApp.Models;

namespace DigitalSignerApp.Utils;

public static partial class CertificateUtils
{
    [GeneratedRegex(@"CN=([^,]+)", RegexOptions.Compiled)]
    private static partial Regex CommonNameRegex();

    public static string ExtractCommonName(string distinguishedName)
    {
        var match = CommonNameRegex().Match(distinguishedName);
        return match.Success ? match.Groups[1].Value.Trim() : distinguishedName;
    }

    public static CertificateInfo ToCertificateInfo(X509Certificate2 cert)
    {
        var (keyType, keySize) = GetKeyInfo(cert);
        
        return new CertificateInfo
        {
            Thumbprint = cert.Thumbprint,
            Subject = cert.Subject,
            Issuer = cert.Issuer,
            NotBefore = cert.NotBefore,
            NotAfter = cert.NotAfter,
            SerialNumber = cert.SerialNumber,
            IsValid = IsValidNow(cert),
            CommonName = ExtractCommonName(cert.Subject),
            KeyType = keyType,
            KeySize = keySize,
            IsHardwareToken = IsHardwareToken(cert)
        };
    }

    public static bool IsValidNow(X509Certificate2 cert)
    {
        var now = DateTime.Now;
        return cert.NotBefore <= now && now <= cert.NotAfter;
    }

    public static bool IsHardwareToken(X509Certificate2 cert)
    {
        if (!cert.HasPrivateKey)
            return false;

        try
        {
            // Try to detect if this is a hardware-backed key
            var rsa = cert.GetRSAPrivateKey();
            if (rsa != null)
            {
                // Hardware tokens typically don't allow key export
                try
                {
                    _ = rsa.ExportParameters(true);
                    return false; // If we can export, it's not hardware-backed
                }
                catch
                {
                    return true; // Can't export = likely hardware
                }
            }

            var ecdsa = cert.GetECDsaPrivateKey();
            if (ecdsa != null)
            {
                try
                {
                    _ = ecdsa.ExportParameters(true);
                    return false;
                }
                catch
                {
                    return true;
                }
            }
        }
        catch
        {
            return true;
        }

        return false;
    }

    public static (string keyType, int keySize) GetKeyInfo(X509Certificate2 cert)
    {
        try
        {
            var rsa = cert.GetRSAPublicKey();
            if (rsa != null)
            {
                return ("RSA", rsa.KeySize);
            }

            var ecdsa = cert.GetECDsaPublicKey();
            if (ecdsa != null)
            {
                return ("ECDSA", ecdsa.KeySize);
            }
        }
        catch
        {
            // Ignore
        }

        return ("Unknown", 0);
    }

    public static bool HasDigitalSignatureUsage(X509Certificate2 cert)
    {
        foreach (var extension in cert.Extensions)
        {
            if (extension is X509KeyUsageExtension keyUsage)
            {
                return (keyUsage.KeyUsages & X509KeyUsageFlags.DigitalSignature) != 0 ||
                       (keyUsage.KeyUsages & X509KeyUsageFlags.NonRepudiation) != 0;
            }
        }
        
        // If no key usage extension, assume it can be used for signing
        return true;
    }
}

6. Service Interfaces

Services/Interfaces/ICertificateService.cs

using System.Security.Cryptography.X509Certificates;
using DigitalSignerApp.Models;

namespace DigitalSignerApp.Services.Interfaces;

public interface ICertificateService
{
    Task<List<X509Certificate2>> GetSigningCertificatesAsync();
    Task<X509Certificate2?> GetCertificateByThumbprintAsync(string thumbprint);
    List<CertificateInfo> GetCertificateInfoList();
    X509Chain BuildCertificateChain(X509Certificate2 certificate);
    bool ValidateCertificate(X509Certificate2 certificate, out List<string> errors);
}

Services/Interfaces/ISignerService.cs

using System.Security.Cryptography.X509Certificates;
using DigitalSignerApp.Models;

namespace DigitalSignerApp.Services.Interfaces;

public interface IPAdESSignerService
{
    Task<SignResponse> SignPdfAsync(byte[] pdfBytes, X509Certificate2 certificate, SignRequest request);
    Task<SignResponse> SignPdfWithTimestampAsync(byte[] pdfBytes, X509Certificate2 certificate, SignRequest request, string tsaUrl);
}

public interface ICAdESSignerService
{
    Task<SignResponse> CreateDetachedSignatureAsync(byte[] data, X509Certificate2 certificate, SignRequest request);
    SignResponse VerifyDetachedSignature(byte[] data, byte[] signature);
}

Services/Interfaces/IWebSocketServer.cs

namespace DigitalSignerApp.Services.Interfaces;

public interface IWebSocketServer : IDisposable
{
    event EventHandler<string>? OnLogMessage;
    event EventHandler<int>? OnConnectionCountChanged;
    
    bool IsRunning { get; }
    int ConnectionCount { get; }
    
    void Start();
    void Stop();
}

7. Certificate Services

Services/CertificateService.cs

using System.Security.Cryptography.X509Certificates;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services.Interfaces;
using DigitalSignerApp.Utils;
using Microsoft.Extensions.Logging;

namespace DigitalSignerApp.Services;

public abstract class CertificateServiceBase : ICertificateService
{
    protected readonly ILogger _logger;

    protected CertificateServiceBase(ILogger logger)
    {
        _logger = logger;
    }

    public abstract Task<List<X509Certificate2>> GetSigningCertificatesAsync();

    public virtual async Task<X509Certificate2?> GetCertificateByThumbprintAsync(string thumbprint)
    {
        var certs = await GetSigningCertificatesAsync();
        return certs.FirstOrDefault(c => 
            c.Thumbprint.Equals(thumbprint, StringComparison.OrdinalIgnoreCase));
    }

    public List<CertificateInfo> GetCertificateInfoList()
    {
        var certs = GetSigningCertificatesAsync().GetAwaiter().GetResult();
        return certs
            .Select(CertificateUtils.ToCertificateInfo)
            .OrderBy(c => c.CommonName)
            .ToList();
    }

    public X509Chain BuildCertificateChain(X509Certificate2 certificate)
    {
        var chain = new X509Chain();
        chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
        chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
        chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
        chain.ChainPolicy.UrlRetrievalTimeout = TimeSpan.FromSeconds(30);
        
        try
        {
            chain.Build(certificate);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to build certificate chain");
        }
        
        return chain;
    }

    public bool ValidateCertificate(X509Certificate2 certificate, out List<string> errors)
    {
        errors = new List<string>();

        if (!certificate.HasPrivateKey)
        {
            errors.Add("Certificate does not have a private key");
        }

        if (!CertificateUtils.IsValidNow(certificate))
        {
            errors.Add($"Certificate is not valid. Valid from {certificate.NotBefore} to {certificate.NotAfter}");
        }

        if (!CertificateUtils.HasDigitalSignatureUsage(certificate))
        {
            errors.Add("Certificate does not have digital signature key usage");
        }

        return errors.Count == 0;
    }

    protected bool IsValidSigningCertificate(X509Certificate2 cert)
    {
        if (!cert.HasPrivateKey)
            return false;

        if (!CertificateUtils.IsValidNow(cert))
            return false;

        return CertificateUtils.HasDigitalSignatureUsage(cert);
    }
}

Services/WindowsCertificateService.cs

using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;

namespace DigitalSignerApp.Services;

public class WindowsCertificateService : CertificateServiceBase
{
    public WindowsCertificateService(ILogger<WindowsCertificateService> logger) 
        : base(logger)
    {
    }

    public override Task<List<X509Certificate2>> GetSigningCertificatesAsync()
    {
        return Task.Run(() =>
        {
            var certificates = new List<X509Certificate2>();

            // Search in CurrentUser store
            try
            {
                using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

                foreach (var cert in store.Certificates)
                {
                    if (IsValidSigningCertificate(cert))
                    {
                        certificates.Add(cert);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error reading CurrentUser certificate store");
            }

            // Search in LocalMachine store
            try
            {
                using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
                store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

                foreach (var cert in store.Certificates)
                {
                    if (IsValidSigningCertificate(cert) &&
                        !certificates.Any(c => c.Thumbprint == cert.Thumbprint))
                    {
                        certificates.Add(cert);
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not access LocalMachine certificate store");
            }

            _logger.LogInformation("Found {Count} signing certificates", certificates.Count);
            return certificates;
        });
    }

    public override async Task<X509Certificate2?> GetCertificateByThumbprintAsync(string thumbprint)
    {
        // First try the base implementation
        var cert = await base.GetCertificateByThumbprintAsync(thumbprint);
        if (cert != null)
            return cert;

        // Try direct store lookup (more efficient)
        try
        {
            using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.ReadOnly);

            var certs = store.Certificates.Find(
                X509FindType.FindByThumbprint,
                thumbprint,
                validOnly: false);

            if (certs.Count > 0)
                return certs[0];
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error finding certificate by thumbprint");
        }

        return null;
    }
}

Services/MacOSCertificateService.cs

using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;

namespace DigitalSignerApp.Services;

public partial class MacOSCertificateService : CertificateServiceBase
{
    public MacOSCertificateService(ILogger<MacOSCertificateService> logger) 
        : base(logger)
    {
    }

    public override async Task<List<X509Certificate2>> GetSigningCertificatesAsync()
    {
        var certificates = new List<X509Certificate2>();

        // Method 1: Use X509Store (works for login keychain)
        try
        {
            using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);

            foreach (var cert in store.Certificates)
            {
                if (IsValidSigningCertificate(cert))
                {
                    certificates.Add(cert);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error reading keychain via X509Store");
        }

        // Method 2: Use security command for more complete access
        try
        {
            var keychainCerts = await GetCertificatesFromKeychainAsync();
            foreach (var cert in keychainCerts)
            {
                if (IsValidSigningCertificate(cert) &&
                    !certificates.Any(c => c.Thumbprint == cert.Thumbprint))
                {
                    certificates.Add(cert);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not access keychain via security command");
        }

        _logger.LogInformation("Found {Count} signing certificates", certificates.Count);
        return certificates;
    }

    private async Task<List<X509Certificate2>> GetCertificatesFromKeychainAsync()
    {
        var certificates = new List<X509Certificate2>();

        try
        {
            // List all identities (certificates with private keys)
            var startInfo = new ProcessStartInfo
            {
                FileName = "/usr/bin/security",
                Arguments = "find-identity -v -p codesigning",
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                UseShellExecute = false,
                CreateNoWindow = true
            };

            using var process = Process.Start(startInfo);
            if (process == null)
                return certificates;

            var output = await process.StandardOutput.ReadToEndAsync();
            await process.WaitForExitAsync();

            // Parse output for certificate hashes
            var matches = IdentityRegex().Matches(output);
            
            foreach (Match match in matches)
            {
                var hash = match.Groups[1].Value;
                var cert = await ExportCertificateFromKeychainAsync(hash);
                if (cert != null)
                {
                    certificates.Add(cert);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error listing keychain identities");
        }

        return certificates;
    }

    private async Task<X509Certificate2?> ExportCertificateFromKeychainAsync(string hash)
    {
        try
        {
            var tempFile = Path.GetTempFileName();
            
            try
            {
                var startInfo = new ProcessStartInfo
                {
                    FileName = "/usr/bin/security",
                    Arguments = $"find-certificate -c \"{hash}\" -p",
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                };

                using var process = Process.Start(startInfo);
                if (process == null)
                    return null;

                var pemData = await process.StandardOutput.ReadToEndAsync();
                await process.WaitForExitAsync();

                if (string.IsNullOrEmpty(pemData))
                    return null;

                // Parse PEM certificate
                var certData = Convert.FromBase64String(
                    pemData
                        .Replace("-----BEGIN CERTIFICATE-----", "")
                        .Replace("-----END CERTIFICATE-----", "")
                        .Replace("\n", "")
                        .Replace("\r", "")
                        .Trim());

                return new X509Certificate2(certData);
            }
            finally
            {
                if (File.Exists(tempFile))
                    File.Delete(tempFile);
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Could not export certificate {Hash}", hash);
            return null;
        }
    }

    [GeneratedRegex(@"\d+\)\s+([A-F0-9]{40})\s+\"([^\"]+)\"", RegexOptions.Compiled)]
    private static partial Regex IdentityRegex();
}

8. Signing Services

Services/PAdESSignerService.cs

using System.Security.Cryptography.X509Certificates;
using iText.Kernel.Pdf;
using iText.Signatures;
using iText.Bouncycastle.Crypto;
using iText.Bouncycastle.X509;
using iText.Commons.Bouncycastle.Cert;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services.Interfaces;
using DigitalSignerApp.Utils;
using iText.Kernel.Geom;
using Microsoft.Extensions.Logging;

namespace DigitalSignerApp.Services;

public class PAdESSignerService : IPAdESSignerService
{
    private readonly ICertificateService _certificateService;
    private readonly ILogger<PAdESSignerService> _logger;

    public PAdESSignerService(
        ICertificateService certificateService,
        ILogger<PAdESSignerService> logger)
    {
        _certificateService = certificateService;
        _logger = logger;
    }

    public async Task<SignResponse> SignPdfAsync(
        byte[] pdfBytes,
        X509Certificate2 certificate,
        SignRequest request)
    {
        return await Task.Run(() =>
        {
            try
            {
                _logger.LogInformation("Starting PAdES signing for request {RequestId}", request.RequestId);

                using var inputStream = new MemoryStream(pdfBytes);
                using var outputStream = new MemoryStream();

                var chain = GetCertificateChain(certificate);
                var pk = GetPrivateKey(certificate);
                var signer = new PrivateKeySignature(pk, DigestAlgorithms.SHA256);

                using var pdfReader = new PdfReader(inputStream);
                var properties = new StampingProperties();

                var pdfSigner = new PdfSigner(pdfReader, outputStream, properties);

                ConfigureSignatureAppearance(pdfSigner, request);

                pdfSigner.SetFieldName($"Sig_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}".Substring(0, 32));

                pdfSigner.SignDetached(
                    signer,
                    chain,
                    null,
                    null,
                    null,
                    0,
                    PdfSigner.CryptoStandard.CADES);

                _logger.LogInformation("PAdES signing completed for request {RequestId}", request.RequestId);

                return new SignResponse
                {
                    RequestId = request.RequestId,
                    Success = true,
                    SignedPdfBase64 = Convert.ToBase64String(outputStream.ToArray()),
                    SignatureType = "PAdES-B",
                    CertificateInfo = CertificateUtils.ToCertificateInfo(certificate),
                    Message = "PDF signed successfully with PAdES"
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "PAdES signing failed for request {RequestId}", request.RequestId);
                return SignResponse.Error(request.RequestId, $"Signing failed: {ex.Message}", "SIGNING_ERROR");
            }
        });
    }

    public async Task<SignResponse> SignPdfWithTimestampAsync(
        byte[] pdfBytes,
        X509Certificate2 certificate,
        SignRequest request,
        string tsaUrl)
    {
        return await Task.Run(() =>
        {
            try
            {
                _logger.LogInformation("Starting PAdES-T signing for request {RequestId}", request.RequestId);

                using var inputStream = new MemoryStream(pdfBytes);
                using var outputStream = new MemoryStream();

                var chain = GetCertificateChain(certificate);
                var pk = GetPrivateKey(certificate);
                var signer = new PrivateKeySignature(pk, DigestAlgorithms.SHA256);

                using var pdfReader = new PdfReader(inputStream);
                var pdfSigner = new PdfSigner(pdfReader, outputStream, new StampingProperties());

                ConfigureSignatureAppearance(pdfSigner, request);

                pdfSigner.SetFieldName($"Sig_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}".Substring(0, 32));

                ITSAClient tsaClient = new TSAClientBouncyCastle(tsaUrl);

                pdfSigner.SignDetached(
                    signer,
                    chain,
                    null,
                    null,
                    tsaClient,
                    0,
                    PdfSigner.CryptoStandard.CADES);

                _logger.LogInformation("PAdES-T signing completed for request {RequestId}", request.RequestId);

                return new SignResponse
                {
                    RequestId = request.RequestId,
                    Success = true,
                    SignedPdfBase64 = Convert.ToBase64String(outputStream.ToArray()),
                    SignatureType = "PAdES-T",
                    CertificateInfo = CertificateUtils.ToCertificateInfo(certificate),
                    Message = "PDF signed successfully with PAdES-T (timestamp)"
                };
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "PAdES-T signing failed for request {RequestId}", request.RequestId);
                return SignResponse.Error(request.RequestId, $"Signing with timestamp failed: {ex.Message}", "SIGNING_ERROR");
            }
        });
    }

    private void ConfigureSignatureAppearance(PdfSigner pdfSigner, SignRequest request)
    {
        var appearance = pdfSigner.GetSignatureAppearance();

        if (!string.IsNullOrEmpty(request.Reason))
            appearance.SetReason(request.Reason);

        if (!string.IsNullOrEmpty(request.Location))
            appearance.SetLocation(request.Location);

        appearance.SetSignatureCreator("DigitalSignerApp");

        if (request.VisibleSignature && request.SignaturePosition != null)
        {
            var pos = request.SignaturePosition;
            var rect = new Rectangle(pos.X, pos.Y, pos.Width, pos.Height);

            appearance.SetPageRect(rect);
            appearance.SetPageNumber(pos.Page);
            appearance.SetRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
        }
    }

    private IX509Certificate[] GetCertificateChain(X509Certificate2 certificate)
    {
        var chain = _certificateService.BuildCertificateChain(certificate);
        var bouncyCastleCerts = new List<IX509Certificate>();

        foreach (var element in chain.ChainElements)
        {
            try
            {
                var bcCert = new X509CertificateParser()
                    .ReadCertificate(element.Certificate.RawData);
                bouncyCastleCerts.Add(new X509CertificateBC(bcCert));
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not parse chain element certificate");
            }
        }

        if (bouncyCastleCerts.Count == 0)
        {
            var bcCert = new X509CertificateParser()
                .ReadCertificate(certificate.RawData);
            bouncyCastleCerts.Add(new X509CertificateBC(bcCert));
        }

        return bouncyCastleCerts.ToArray();
    }

    private IPrivateKey GetPrivateKey(X509Certificate2 certificate)
    {
        var rsa = certificate.GetRSAPrivateKey();
        if (rsa != null)
        {
            try
            {
                var rsaParams = rsa.ExportParameters(true);
                var bcRsaParams = new Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters(
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Modulus),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Exponent),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.D),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.P),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.Q),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.DP),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.DQ),
                    new Org.BouncyCastle.Math.BigInteger(1, rsaParams.InverseQ));

                return new PrivateKeyBC(bcRsaParams);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not export RSA key, attempting CSP access");
                // For hardware tokens, we need a different approach
                throw new InvalidOperationException(
                    "Hardware token detected. Please ensure the token is properly connected and PIN is entered.");
            }
        }

        var ecdsa = certificate.GetECDsaPrivateKey();
        if (ecdsa != null)
        {
            try
            {
                var ecParams = ecdsa.ExportParameters(true);
                var domainParams = ECNamedCurveTable.GetByName(GetCurveName(ecParams));
                
                if (domainParams != null)
                {
                    var ecPoint = domainParams.Curve.CreatePoint(
                        new Org.BouncyCastle.Math.BigInteger(1, ecParams.Q.X),
                        new Org.BouncyCastle.Math.BigInteger(1, ecParams.Q.Y));
                    
                    var bcEcParams = new Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters(
                        new Org.BouncyCastle.Math.BigInteger(1, ecParams.D),
                        new Org.BouncyCastle.Crypto.Parameters.ECDomainParameters(
                            domainParams.Curve, domainParams.G, domainParams.N, domainParams.H));

                    return new PrivateKeyBC(bcEcParams);
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not export ECDSA key");
            }
        }

        throw new InvalidOperationException("Could not extract private key from certificate");
    }

    private string GetCurveName(System.Security.Cryptography.ECParameters ecParams)
    {
        return ecParams.Curve.Oid?.FriendlyName switch
        {
            "nistP256" or "ECDSA_P256" => "P-256",
            "nistP384" or "ECDSA_P384" => "P-384",
            "nistP521" or "ECDSA_P521" => "P-521",
            _ => "P-256"
        };
    }
}

Services/CAdESSignerService.cs

using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services.Interfaces;
using DigitalSignerApp.Utils;
using Microsoft.Extensions.Logging;
using Org.BouncyCastle.Cms;
using Org.BouncyCastle.X509.Store;
using BcX509 = Org.BouncyCastle.X509;

namespace DigitalSignerApp.Services;

public class CAdESSignerService : ICAdESSignerService
{
    private readonly ICertificateService _certificateService;
    private readonly ILogger<CAdESSignerService> _logger;

    public CAdESSignerService(
        ICertificateService certificateService,
        ILogger<CAdESSignerService> logger)
    {
        _certificateService = certificateService;
        _logger = logger;
    }

    public async Task<SignResponse> CreateDetachedSignatureAsync(
        byte[] data,
        X509Certificate2 certificate,
        SignRequest request)
    {
        return await Task.Run(() => CreateDetachedSignature(data, certificate, request));
    }

    private SignResponse CreateDetachedSignature(
        byte[] data,
        X509Certificate2 certificate,
        SignRequest request)
    {
        try
        {
            _logger.LogInformation("Creating CAdES detached signature for request {RequestId}", request.RequestId);

            var contentInfo = new ContentInfo(data);
            var signedCms = new SignedCms(contentInfo, detached: true);

            var signer = new CmsSigner(SubjectIdentifierType.IssuerAndSerialNumber, certificate)
            {
                DigestAlgorithm = new Oid("2.16.840.1.101.3.4.2.1", "SHA256"),
                IncludeOption = X509IncludeOption.WholeChain
            };

            // Add signing time attribute
            signer.SignedAttributes.Add(new Pkcs9SigningTime(DateTime.UtcNow));

            // Add content type attribute
            signer.SignedAttributes.Add(
                new AsnEncodedData(new Oid("1.2.840.113549.1.9.3"), 
                    new byte[] { 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x01 }));

            // Compute signature - triggers PIN prompt for hardware tokens
            signedCms.ComputeSignature(signer, silent: false);

            byte[] signature = signedCms.Encode();

            _logger.LogInformation("CAdES signature created for request {RequestId}", request.RequestId);

            return new SignResponse
            {
                RequestId = request.RequestId,
                Success = true,
                SignatureBase64 = Convert.ToBase64String(signature),
                SignatureType = "CAdES-BES",
                CertificateInfo = CertificateUtils.ToCertificateInfo(certificate),
                Message = "CAdES detached signature created successfully"
            };
        }
        catch (CryptographicException ex) when (ex.Message.Contains("cancelled") || 
                                                  ex.Message.Contains("canceled"))
        {
            _logger.LogWarning("User cancelled PIN entry for request {RequestId}", request.RequestId);
            return SignResponse.Error(request.RequestId, "Signing cancelled by user", "USER_CANCELLED");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "CAdES signing failed for request {RequestId}", request.RequestId);
            return SignResponse.Error(request.RequestId, $"Signing failed: {ex.Message}", "SIGNING_ERROR");
        }
    }

    public SignResponse VerifyDetachedSignature(byte[] data, byte[] signature)
    {
        try
        {
            _logger.LogInformation("Verifying CAdES detached signature");

            var contentInfo = new ContentInfo(data);
            var signedCms = new SignedCms(contentInfo, detached: true);

            signedCms.Decode(signature);
            signedCms.CheckSignature(verifySignatureOnly: false);

            var signerInfo = signedCms.SignerInfos[0];
            var signerCert = signerInfo.Certificate;

            // Extract signing time
            DateTime? signingTime = null;
            foreach (var attr in signerInfo.SignedAttributes)
            {
                if (attr.Oid.Value == "1.2.840.113549.1.9.5") // signingTime OID
                {
                    var pkcs9Time = new Pkcs9SigningTime(attr.Values[0].RawData);
                    signingTime = pkcs9Time.SigningTime;
                    break;
                }
            }

            _logger.LogInformation("Signature verification successful");

            return new SignResponse
            {
                Success = true,
                Message = signingTime.HasValue 
                    ? $"Signature is valid. Signed at: {signingTime.Value:u}"
                    : "Signature is valid",
                CertificateInfo = signerCert != null
                    ? CertificateUtils.ToCertificateInfo(signerCert)
                    : null
            };
        }
        catch (CryptographicException ex)
        {
            _logger.LogWarning(ex, "Signature verification failed");
            return SignResponse.Error("", $"Signature verification failed: {ex.Message}", "VERIFICATION_FAILED");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error during signature verification");
            return SignResponse.Error("", $"Error: {ex.Message}", "VERIFICATION_ERROR");
        }
    }
}

9. WebSocket Server

Services/WebSocketServer.cs

using System.Collections.Concurrent;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services.Interfaces;
using Fleck;
using Microsoft.Extensions.Logging;

namespace DigitalSignerApp.Services;

public class SigningWebSocketServer : IWebSocketServer
{
    private WebSocketServer? _server;
    private readonly ConcurrentDictionary<Guid, IWebSocketConnection> _connections = new();
    private readonly ICertificateService _certificateService;
    private readonly IPAdESSignerService _padesSignerService;
    private readonly ICAdESSignerService _cadesSignerService;
    private readonly ILogger<SigningWebSocketServer> _logger;
    private readonly AppSettings _settings;
    private readonly JsonSerializerOptions _jsonOptions;

    public event EventHandler<string>? OnLogMessage;
    public event EventHandler<int>? OnConnectionCountChanged;
    public event Func<List<X509Certificate2>, Task<X509Certificate2?>>? OnCertificateSelectionRequired;

    public bool IsRunning { get; private set; }
    public int ConnectionCount => _connections.Count;

    public SigningWebSocketServer(
        ICertificateService certificateService,
        IPAdESSignerService padesSignerService,
        ICAdESSignerService cadesSignerService,
        AppSettings settings,
        ILogger<SigningWebSocketServer> logger)
    {
        _certificateService = certificateService;
        _padesSignerService = padesSignerService;
        _cadesSignerService = cadesSignerService;
        _settings = settings;
        _logger = logger;

        _jsonOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
            WriteIndented = false
        };
    }

    public void Start()
    {
        if (IsRunning)
        {
            Log("Server is already running");
            return;
        }

        try
        {
            var protocol = _settings.EnableSecureWebSocket ? "wss" : "ws";
            var url = $"{protocol}://127.0.0.1:{_settings.WebSocketPort}";

            _server = new WebSocketServer(url);

            if (_settings.EnableSecureWebSocket && !string.IsNullOrEmpty(_settings.SslCertificatePath))
            {
                _server.Certificate = new X509Certificate2(
                    _settings.SslCertificatePath,
                    _settings.SslCertificatePassword);
            }

            _server.RestartAfterListenError = true;

            _server.Start(ConfigureSocket);

            IsRunning = true;
            Log($"WebSocket server started on {url}");
            _logger.LogInformation("WebSocket server started on {Url}", url);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to start WebSocket server");
            Log($"Failed to start server: {ex.Message}");
            throw;
        }
    }

    public void Stop()
    {
        if (!IsRunning)
            return;

        try
        {
            // Close all connections
            foreach (var connection in _connections.Values)
            {
                try
                {
                    connection.Close();
                }
                catch { }
            }
            _connections.Clear();

            _server?.Dispose();
            _server = null;

            IsRunning = false;
            Log("WebSocket server stopped");
            _logger.LogInformation("WebSocket server stopped");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error stopping WebSocket server");
        }
    }

    private void ConfigureSocket(IWebSocketConnection socket)
    {
        var connectionId = Guid.NewGuid();

        socket.OnOpen = () =>
        {
            _connections[connectionId] = socket;
            Log($"Client connected: {connectionId}");
            OnConnectionCountChanged?.Invoke(this, ConnectionCount);

            // Send welcome message
            var welcome = new SignResponse
            {
                Success = true,
                Message = "Connected to DigitalSignerApp",
                Version = "1.0.0",
                Timestamp = DateTime.UtcNow
            };
            SendResponse(socket, welcome);
        };

        socket.OnClose = () =>
        {
            _connections.TryRemove(connectionId, out _);
            Log($"Client disconnected: {connectionId}");
            OnConnectionCountChanged?.Invoke(this, ConnectionCount);
        };

        socket.OnMessage = async message =>
        {
            await HandleMessageAsync(socket, message);
        };

        socket.OnError = ex =>
        {
            _logger.LogError(ex, "WebSocket error for connection {ConnectionId}", connectionId);
            Log($"WebSocket error: {ex.Message}");
        };
    }

    private async Task HandleMessageAsync(IWebSocketConnection socket, string message)
    {
        SignResponse response;

        try
        {
            var request = JsonSerializer.Deserialize<SignRequest>(message, _jsonOptions);

            if (request == null)
            {
                response = SignResponse.Error("", "Invalid request format", "INVALID_REQUEST");
                SendResponse(socket, response);
                return;
            }

            Log($"Request: {request.Action} (ID: {request.RequestId})");
            _logger.LogInformation("Processing request {Action} with ID {RequestId}",
                request.Action, request.RequestId);

            response = request.Action?.ToLower() switch
            {
                "list_certificates" => HandleListCertificates(request),
                "sign_pades" => await HandleSignPAdESAsync(request),
                "sign_cades" => await HandleSignCAdESAsync(request),
                "sign" => await HandleAutoSignAsync(request),
                "verify_cades" => HandleVerifyCAdES(request),
                "ping" => SignResponse.Ok(request.RequestId, "pong"),
                "health" => HandleHealthCheck(request),
                _ => SignResponse.Error(request.RequestId, $"Unknown action: {request.Action}", "UNKNOWN_ACTION")
            };
        }
        catch (JsonException ex)
        {
            _logger.LogWarning(ex, "Invalid JSON received");
            response = SignResponse.Error("", "Invalid JSON format", "JSON_ERROR");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing request");
            response = SignResponse.Error("", $"Internal error: {ex.Message}", "INTERNAL_ERROR");
        }

        SendResponse(socket, response);
    }

    private SignResponse HandleListCertificates(SignRequest request)
    {
        try
        {
            var certificates = _certificateService.GetCertificateInfoList();

            return new SignResponse
            {
                RequestId = request.RequestId,
                Success = true,
                Certificates = certificates,
                Message = $"Found {certificates.Count} signing certificate(s)"
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error listing certificates");
            return SignResponse.Error(request.RequestId, ex.Message, "CERTIFICATE_ERROR");
        }
    }

    private async Task<SignResponse> HandleSignPAdESAsync(SignRequest request)
    {
        var pdfData = request.PdfBase64 ?? request.DataBase64;
        
        if (string.IsNullOrEmpty(pdfData))
        {
            return SignResponse.Error(request.RequestId, "PDF data is required", "MISSING_DATA");
        }

        var certificate = await GetCertificateAsync(request.CertificateThumbprint);
        if (certificate == null)
        {
            return SignResponse.Error(request.RequestId, "No certificate selected or found", "NO_CERTIFICATE");
        }

        try
        {
            var pdfBytes = Convert.FromBase64String(pdfData);

            if (request.IncludeTimestamp && !string.IsNullOrEmpty(request.TsaUrl ?? _settings.DefaultTsaUrl))
            {
                return await _padesSignerService.SignPdfWithTimestampAsync(
                    pdfBytes,
                    certificate,
                    request,
                    request.TsaUrl ?? _settings.DefaultTsaUrl!);
            }

            return await _padesSignerService.SignPdfAsync(pdfBytes, certificate, request);
        }
        catch (FormatException)
        {
            return SignResponse.Error(request.RequestId, "Invalid Base64 data", "INVALID_BASE64");
        }
    }

    private async Task<SignResponse> HandleSignCAdESAsync(SignRequest request)
    {
        var data = request.DataBase64 ?? request.PdfBase64;
        
        if (string.IsNullOrEmpty(data))
        {
            return SignResponse.Error(request.RequestId, "Data to sign is required", "MISSING_DATA");
        }

        var certificate = await GetCertificateAsync(request.CertificateThumbprint);
        if (certificate == null)
        {
            return SignResponse.Error(request.RequestId, "No certificate selected or found", "NO_CERTIFICATE");
        }

        try
        {
            var dataBytes = Convert.FromBase64String(data);
            return await _cadesSignerService.CreateDetachedSignatureAsync(dataBytes, certificate, request);
        }
        catch (FormatException)
        {
            return SignResponse.Error(request.RequestId, "Invalid Base64 data", "INVALID_BASE64");
        }
    }

    private async Task<SignResponse> HandleAutoSignAsync(SignRequest request)
    {
        return request.SignatureType?.ToLower() switch
        {
            "cades" => await HandleSignCAdESAsync(request),
            _ => await HandleSignPAdESAsync(request)
        };
    }

    private SignResponse HandleVerifyCAdES(SignRequest request)
    {
        if (string.IsNullOrEmpty(request.DataBase64) || string.IsNullOrEmpty(request.PdfBase64))
        {
            return SignResponse.Error(request.RequestId,
                "Both data and signature are required for verification", "MISSING_DATA");
        }

        try
        {
            var data = Convert.FromBase64String(request.DataBase64);
            var signature = Convert.FromBase64String(request.PdfBase64);

            var result = _cadesSignerService.VerifyDetachedSignature(data, signature);
            result.RequestId = request.RequestId;
            return result;
        }
        catch (FormatException)
        {
            return SignResponse.Error(request.RequestId, "Invalid Base64 data", "INVALID_BASE64");
        }
    }

    private SignResponse HandleHealthCheck(SignRequest request)
    {
        return new SignResponse
        {
            RequestId = request.RequestId,
            Success = true,
            Message = "Service is healthy",
            Timestamp = DateTime.UtcNow,
            Version = "1.0.0"
        };
    }

    private async Task<X509Certificate2?> GetCertificateAsync(string? thumbprint)
    {
        if (!string.IsNullOrEmpty(thumbprint))
        {
            return await _certificateService.GetCertificateByThumbprintAsync(thumbprint);
        }

        if (OnCertificateSelectionRequired != null)
        {
            var certificates = await _certificateService.GetSigningCertificatesAsync();
            
            if (certificates.Count == 0)
            {
                Log("No signing certificates found");
                return null;
            }

            if (certificates.Count == 1)
            {
                return certificates[0];
            }

            return await OnCertificateSelectionRequired.Invoke(certificates);
        }

        var certs = await _certificateService.GetSigningCertificatesAsync();
        return certs.FirstOrDefault();
    }

    private void SendResponse(IWebSocketConnection socket, SignResponse response)
    {
        try
        {
            var json = JsonSerializer.Serialize(response, _jsonOptions);
            socket.Send(json);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error sending response");
        }
    }

    private void Log(string message)
    {
        var logMessage = $"[{DateTime.Now:HH:mm:ss}] {message}";
        OnLogMessage?.Invoke(this, logMessage);
    }

    public void Dispose()
    {
        Stop();
    }
}

10. ViewModels

ViewModels/ViewModelBase.cs

using CommunityToolkit.Mvvm.ComponentModel;

namespace DigitalSignerApp.ViewModels;

public abstract class ViewModelBase : ObservableObject
{
}

ViewModels/MainWindowViewModel.cs

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services.Interfaces;

namespace DigitalSignerApp.ViewModels;

public partial class MainWindowViewModel : ViewModelBase
{
    private readonly IWebSocketServer _webSocketServer;
    private readonly ICertificateService _certificateService;
    private readonly AppSettings _settings;

    [ObservableProperty]
    private bool _isServerRunning;

    [ObservableProperty]
    private int _connectionCount;

    [ObservableProperty]
    private string _statusText = "Stopped";

    [ObservableProperty]
    private ObservableCollection<string> _logMessages = new();

    public int Port => _settings.WebSocketPort;

    public MainWindowViewModel(
        IWebSocketServer webSocketServer,
        ICertificateService certificateService,
        AppSettings settings)
    {
        _webSocketServer = webSocketServer;
        _certificateService = certificateService;
        _settings = settings;

        _webSocketServer.OnLogMessage += (_, msg) =>
        {
            LogMessages.Add(msg);
            // Keep only last 1000 messages
            while (LogMessages.Count > 1000)
            {
                LogMessages.RemoveAt(0);
            }
        };

        _webSocketServer.OnConnectionCountChanged += (_, count) =>
        {
            ConnectionCount = count;
        };
    }

    [RelayCommand]
    private void StartServer()
    {
        try
        {
            _webSocketServer.Start();
            IsServerRunning = true;
            StatusText = "Running";
        }
        catch (Exception ex)
        {
            LogMessages.Add($"[ERROR] Failed to start server: {ex.Message}");
        }
    }

    [RelayCommand]
    private void StopServer()
    {
        _webSocketServer.Stop();
        IsServerRunning = false;
        StatusText = "Stopped";
    }

    [RelayCommand]
    private void ClearLog()
    {
        LogMessages.Clear();
    }

    [RelayCommand]
    private async Task ViewCertificates()
    {
        try
        {
            var certs = await _certificateService.GetSigningCertificatesAsync();
            LogMessages.Add($"Found {certs.Count} signing certificates");
            
            foreach (var cert in certs)
            {
                var info = Utils.CertificateUtils.ToCertificateInfo(cert);
                LogMessages.Add($"  - {info.CommonName} (Valid until: {info.NotAfter:d})");
            }
        }
        catch (Exception ex)
        {
            LogMessages.Add($"[ERROR] Failed to list certificates: {ex.Message}");
        }
    }

    public void Initialize()
    {
        if (_settings.AutoStart)
        {
            StartServer();
        }
    }

    public void Shutdown()
    {
        StopServer();
    }
}

ViewModels/CertificateSelectionViewModel.cs

using System.Collections.ObjectModel;
using System.Security.Cryptography.X509Certificates;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DigitalSignerApp.Models;
using DigitalSignerApp.Utils;

namespace DigitalSignerApp.ViewModels;

public partial class CertificateSelectionViewModel : ViewModelBase
{
    private readonly List<X509Certificate2> _certificates;

    [ObservableProperty]
    private ObservableCollection<CertificateDisplayItem> _certificateItems = new();

    [ObservableProperty]
    private CertificateDisplayItem? _selectedItem;

    [ObservableProperty]
    private string _detailsText = "";

    public X509Certificate2? SelectedCertificate { get; private set; }
    public bool WasConfirmed { get; private set; }

    public CertificateSelectionViewModel(List<X509Certificate2> certificates)
    {
        _certificates = certificates;

        foreach (var cert in certificates)
        {
            CertificateItems.Add(new CertificateDisplayItem(cert));
        }

        if (CertificateItems.Any())
        {
            SelectedItem = CertificateItems[0];
        }
    }

    partial void OnSelectedItemChanged(CertificateDisplayItem? value)
    {
        if (value != null)
        {
            DetailsText = $"Subject: {value.Subject}\n" +
                         $"Serial Number: {value.SerialNumber}\n" +
                         $"Thumbprint: {value.Thumbprint}\n" +
                         $"Key Type: {value.KeyType}";
        }
        else
        {
            DetailsText = "";
        }
    }

    [RelayCommand]
    private void Confirm()
    {
        if (SelectedItem != null)
        {
            SelectedCertificate = _certificates.First(c => c.Thumbprint == SelectedItem.Thumbprint);
            WasConfirmed = true;
        }
    }

    [RelayCommand]
    private void Cancel()
    {
        WasConfirmed = false;
    }
}

public class CertificateDisplayItem
{
    private readonly X509Certificate2 _certificate;

    public CertificateDisplayItem(X509Certificate2 certificate)
    {
        _certificate = certificate;
        var info = CertificateUtils.ToCertificateInfo(certificate);
        CommonName = info.CommonName;
        Issuer = CertificateUtils.ExtractCommonName(info.Issuer);
        NotAfter = info.NotAfter;
        IsValid = info.IsValid;
        Thumbprint = info.Thumbprint;
        Subject = info.Subject;
        SerialNumber = info.SerialNumber;
        KeyType = $"{info.KeyType} {info.KeySize}";
        IsHardwareToken = info.IsHardwareToken;
    }

    public string CommonName { get; }
    public string Issuer { get; }
    public DateTime NotAfter { get; }
    public bool IsValid { get; }
    public string Thumbprint { get; }
    public string Subject { get; }
    public string SerialNumber { get; }
    public string KeyType { get; }
    public bool IsHardwareToken { get; }

    public string StatusText => IsValid ? "Valid" : "Invalid";
    public string TokenText => IsHardwareToken ? "🔐" : "";
}

11. Views

App.axaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="DigitalSignerApp.App"
             RequestedThemeVariant="Default">

    <Application.Styles>
        <FluentTheme />
        <StyleInclude Source="avares://DigitalSignerApp/Styles/AppStyles.axaml"/>
    </Application.Styles>
</Application>

App.axaml.cs

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using DigitalSignerApp.Models;
using DigitalSignerApp.Services;
using DigitalSignerApp.Services.Interfaces;
using DigitalSignerApp.Utils;
using DigitalSignerApp.ViewModels;
using DigitalSignerApp.Views;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;

namespace DigitalSignerApp;

public partial class App : Application
{
    private IServiceProvider? _serviceProvider;

    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        ConfigureServices();

        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            var mainWindow = new MainWindow
            {
                DataContext = _serviceProvider!.GetRequiredService<MainWindowViewModel>()
            };

            // Configure certificate selection callback
            var wsServer = _serviceProvider!.GetRequiredService<SigningWebSocketServer>();
            wsServer.OnCertificateSelectionRequired += async (certificates) =>
            {
                return await Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() =>
                {
                    var dialog = new CertificateSelectionDialog(certificates);
                    dialog.ShowDialog(mainWindow);
                    
                    if (dialog.DialogResult == true)
                    {
                        return dialog.SelectedCertificate;
                    }
                    return null;
                });
            };

            desktop.MainWindow = mainWindow;

            desktop.ShutdownRequested += (_, _) =>
            {
                var vm = mainWindow.DataContext as MainWindowViewModel;
                vm?.Shutdown();
            };
        }

        base.OnFrameworkInitializationCompleted();
    }

    private void ConfigureServices()
    {
        var services = new ServiceCollection();

        // Configuration
        var settings = LoadSettings();
        services.AddSingleton(settings);

        // Logging
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Information()
            .WriteTo.File(
                Path.Combine(PlatformHelper.GetLogPath(), "app-.log"),
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: settings.MaxLogRetentionDays)
            .CreateLogger();

        services.AddLogging(builder =>
        {
            builder.AddSerilog(dispose: true);
        });

        // Services - Platform specific
        if (PlatformHelper.IsWindows)
        {
            services.AddSingleton<ICertificateService, WindowsCertificateService>();
        }
        else if (PlatformHelper.IsMacOS)
        {
            services.AddSingleton<ICertificateService, MacOSCertificateService>();
        }
        else
        {
            // Linux fallback
            services.AddSingleton<ICertificateService, WindowsCertificateService>();
        }

        services.AddSingleton<IPAdESSignerService, PAdESSignerService>();
        services.AddSingleton<ICAdESSignerService, CAdESSignerService>();
        services.AddSingleton<SigningWebSocketServer>();
        services.AddSingleton<IWebSocketServer>(sp => sp.GetRequiredService<SigningWebSocketServer>());

        // ViewModels
        services.AddTransient<MainWindowViewModel>();

        _serviceProvider = services.BuildServiceProvider();
    }

    private AppSettings LoadSettings()
    {
        var settingsPath = PlatformHelper.GetSettingsPath();
        
        if (File.Exists(settingsPath))
        {
            try
            {
                var json = File.ReadAllText(settingsPath);
                return System.Text.Json.JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
            }
            catch
            {
                return new AppSettings();
            }
        }

        var settings = new AppSettings();
        SaveSettings(settings);
        return settings;
    }

    private void SaveSettings(AppSettings settings)
    {
        try
        {
            var json = System.Text.Json.JsonSerializer.Serialize(settings, new System.Text.Json.JsonSerializerOptions
            {
                WriteIndented = true
            });
            File.WriteAllText(PlatformHelper.GetSettingsPath(), json);
        }
        catch
        {
            // Ignore save errors
        }
    }
}

Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:DigitalSignerApp.ViewModels"
        x:Class="DigitalSignerApp.Views.MainWindow"
        x:DataType="vm:MainWindowViewModel"
        Title="Digital Signer App"
        Width="800" Height="600"
        MinWidth="600" MinHeight="400"
        WindowStartupLocation="CenterScreen">
    
    <Design.DataContext>
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <!-- Header -->
        <StackPanel Grid.Row="0" Margin="0,0,0,16">
            <TextBlock Text="Digital Signer App" 
                       FontSize="24" 
                       FontWeight="Bold"/>
            <TextBlock Text="PDF signing service with PAdES and CAdES support"
                       Foreground="Gray"
                       Margin="0,4,0,0"/>
        </StackPanel>

        <!-- Status Panel -->
        <Border Grid.Row="1" 
                Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
                CornerRadius="8"
                Padding="16"
                Margin="0,0,0,16">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <StackPanel Grid.Column="0">
                    <StackPanel Orientation="Horizontal">
                        <Ellipse Width="12" Height="12"
                                 Fill="{Binding IsServerRunning, Converter={StaticResource BoolToColorConverter}}"
                                 Margin="0,0,8,0"/>
                        <TextBlock Text="{Binding StatusText}"
                                   VerticalAlignment="Center"
                                   FontWeight="SemiBold"/>
                    </StackPanel>
                    <TextBlock Text="{Binding Port, StringFormat='Port: {0}'}"
                               Foreground="Gray"
                               Margin="20,4,0,0"/>
                    <TextBlock Text="{Binding ConnectionCount, StringFormat='Connections: {0}'}"
                               Foreground="Gray"
                               Margin="20,2,0,0"/>
                </StackPanel>

                <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
                    <Button Content="Start Server"
                            Command="{Binding StartServerCommand}"
                            IsEnabled="{Binding !IsServerRunning}"
                            Classes="Primary"/>
                    <Button Content="Stop Server"
                            Command="{Binding StopServerCommand}"
                            IsEnabled="{Binding IsServerRunning}"/>
                </StackPanel>
            </Grid>
        </Border>

        <!-- Log Panel -->
        <Border Grid.Row="2"
                Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
                CornerRadius="8"
                Padding="8">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>

                <TextBlock Grid.Row="0" 
                           Text="Activity Log"
                           FontWeight="SemiBold"
                           Margin="8,0,0,8"/>

                <ScrollViewer Grid.Row="1"
                              Name="LogScrollViewer"
                              HorizontalScrollBarVisibility="Auto"
                              VerticalScrollBarVisibility="Auto">
                    <ItemsControl ItemsSource="{Binding LogMessages}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding}"
                                           FontFamily="Consolas, Menlo, monospace"
                                           FontSize="11"
                                           Margin="0,1"/>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ScrollViewer>
            </Grid>
        </Border>

        <!-- Bottom Actions -->
        <StackPanel Grid.Row="3" 
                    Orientation="Horizontal" 
                    HorizontalAlignment="Right"
                    Spacing="8"
                    Margin="0,16,0,0">
            <Button Content="View Certificates" 
                    Command="{Binding ViewCertificatesCommand}"/>
            <Button Content="Clear Log" 
                    Command="{Binding ClearLogCommand}"/>
            <Button Content="Minimize to Tray"
                    Click="MinimizeToTray_Click"/>
        </StackPanel>
    </Grid>
</Window>

Views/MainWindow.axaml.cs

using Avalonia.Controls;
using Avalonia.Interactivity;
using DigitalSignerApp.ViewModels;

namespace DigitalSignerApp.Views;

public partial class MainWindow : Window
{
    private bool _isExiting;

    public MainWindow()
    {
        InitializeComponent();

        Loaded += OnLoaded;
        Closing += OnClosing;
    }

    private void OnLoaded(object? sender, RoutedEventArgs e)
    {
        if (DataContext is MainWindowViewModel vm)
        {
            vm.Initialize();
        }
    }

    private void OnClosing(object? sender, WindowClosingEventArgs e)
    {
        if (!_isExiting)
        {
            e.Cancel = true;
            Hide();
        }
    }

    private void MinimizeToTray_Click(object? sender, RoutedEventArgs e)
    {
        Hide();
    }

    public void ExitApplication()
    {
        _isExiting = true;
        Close();
    }

    public void ShowAndActivate()
    {
        Show();
        Activate();
        WindowState = WindowState.Normal;
    }
}

Views/CertificateSelectionDialog.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:DigitalSignerApp.ViewModels"
        x:Class="DigitalSignerApp.Views.CertificateSelectionDialog"
        x:DataType="vm:CertificateSelectionViewModel"
        Title="Select Certificate"
        Width="650" Height="450"
        MinWidth="500" MinHeight="350"
        WindowStartupLocation="CenterOwner"
        CanResize="True">

    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>

        <TextBlock Grid.Row="0" 
                   Text="Select a certificate for digital signature:"
                   FontWeight="SemiBold"
                   Margin="0,0,0,12"/>

        <DataGrid Grid.Row="1"
                  ItemsSource="{Binding CertificateItems}"
                  SelectedItem="{Binding SelectedItem}"
                  AutoGenerateColumns="False"
                  IsReadOnly="True"
                  CanUserReorderColumns="False"
                  CanUserResizeColumns="True"
                  SelectionMode="Single"
                  GridLinesVisibility="Horizontal"
                  BorderThickness="1"
                  CornerRadius="4">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" 
                                    Binding="{Binding CommonName}" 
                                    Width="200"/>
                <DataGridTextColumn Header="Issuer" 
                                    Binding="{Binding Issuer}" 
                                    Width="150"/>
                <DataGridTextColumn Header="Valid Until" 
                                    Binding="{Binding NotAfter, StringFormat={}{0:d}}" 
                                    Width="100"/>
                <DataGridTextColumn Header="Status" 
                                    Binding="{Binding StatusText}" 
                                    Width="70"/>
                <DataGridTextColumn Header="" 
                                    Binding="{Binding TokenText}" 
                                    Width="30"/>
            </DataGrid.Columns>
        </DataGrid>

        <Border Grid.Row="2"
                Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
                CornerRadius="4"
                Padding="8"
                Margin="0,12,0,0"
                MinHeight="60">
            <TextBlock Text="{Binding DetailsText}"
                       TextWrapping="Wrap"
                       FontSize="11"
                       Foreground="Gray"/>
        </Border>

        <StackPanel Grid.Row="3"
                    Orientation="Horizontal"
                    HorizontalAlignment="Right"
                    Spacing="8"
                    Margin="0,16,0,0">
            <Button Content="Select"
                    Command="{Binding ConfirmCommand}"
                    IsDefault="True"
                    Classes="Primary"
                    Click="SelectButton_Click"/>
            <Button Content="Cancel"
                    Command="{Binding CancelCommand}"
                    IsCancel="True"
                    Click="CancelButton_Click"/>
        </StackPanel>
    </Grid>
</Window>

Views/CertificateSelectionDialog.axaml.cs

using System.Security.Cryptography.X509Certificates;
using Avalonia.Controls;
using Avalonia.Interactivity;
using DigitalSignerApp.ViewModels;

namespace DigitalSignerApp.Views;

public partial class CertificateSelectionDialog : Window
{
    private readonly CertificateSelectionViewModel _viewModel;

    public bool? DialogResult { get; private set; }
    public X509Certificate2? SelectedCertificate => _viewModel.SelectedCertificate;

    public CertificateSelectionDialog(List<X509Certificate2> certificates)
    {
        _viewModel = new CertificateSelectionViewModel(certificates);
        DataContext = _viewModel;

        InitializeComponent();
    }

    private void SelectButton_Click(object? sender, RoutedEventArgs e)
    {
        if (_viewModel.WasConfirmed)
        {
            DialogResult = true;
            Close();
        }
    }

    private void CancelButton_Click(object? sender, RoutedEventArgs e)
    {
        DialogResult = false;
        Close();
    }
}

Styles/AppStyles.axaml

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!-- Bool to Color Converter -->
    <Style>
        <Style.Resources>
            <SolidColorBrush x:Key="GreenBrush" Color="#10B981"/>
            <SolidColorBrush x:Key="RedBrush" Color="#EF4444"/>
        </Style.Resources>
    </Style>

    <!-- Primary Button Style -->
    <Style Selector="Button.Primary">
        <Setter Property="Background" Value="#3B82F6"/>
        <Setter Property="Foreground" Value="White"/>
    </Style>

    <Style Selector="Button.Primary:pointerover /template/ ContentPresenter">
        <Setter Property="Background" Value="#2563EB"/>
    </Style>

    <!-- Button Base Style -->
    <Style Selector="Button">
        <Setter Property="Padding" Value="16,8"/>
        <Setter Property="CornerRadius" Value="4"/>
    </Style>
</Styles>

12. Value Converters (add to App.axaml.cs)

// Add this converter class
using Avalonia.Data.Converters;
using Avalonia.Media;

namespace DigitalSignerApp.Converters;

public class BoolToColorConverter : IValueConverter
{
    public static readonly BoolToColorConverter Instance = new();

    public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is bool b)
        {
            return b ? new SolidColorBrush(Color.Parse("#10B981")) : new SolidColorBrush(Color.Parse("#EF4444"));
        }
        return new SolidColorBrush(Colors.Gray);
    }

    public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Update App.axaml to include the converter:

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:converters="using:DigitalSignerApp.Converters"
             x:Class="DigitalSignerApp.App"
             RequestedThemeVariant="Default">

    <Application.Resources>
        <converters:BoolToColorConverter x:Key="BoolToColorConverter"/>
    </Application.Resources>

    <Application.Styles>
        <FluentTheme />
    </Application.Styles>
</Application>

13. JavaScript Client (Updated)

/**
 * Cross-platform Digital Signer Client
 * Compatible with Windows and macOS signing service
 */
class DigitalSignerClient {
    constructor(options = {}) {
        this.port = options.port || 9876;
        this.secure = options.secure || false;
        this.timeout = options.timeout || 300000; // 5 minutes for PIN entry
        this.ws = null;
        this.pendingRequests = new Map();
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = options.maxReconnectAttempts || 3;
        this.reconnectDelay = options.reconnectDelay || 1000;
        
        // Event callbacks
        this.onStatusChange = null;
        this.onError = null;
        this.onLog = null;
    }

    get isConnected() {
        return this.ws?.readyState === WebSocket.OPEN;
    }

    async connect() {
        if (this.isConnected) {
            return;
        }

        return new Promise((resolve, reject) => {
            const protocol = this.secure ? 'wss' : 'ws';
            const url = `${protocol}://127.0.0.1:${this.port}`;
            
            this.log(`Connecting to ${url}...`);
            
            try {
                this.ws = new WebSocket(url);
            } catch (error) {
                reject(new Error(`Failed to create WebSocket: ${error.message}`));
                return;
            }

            const connectionTimeout = setTimeout(() => {
                this.ws?.close();
                reject(new Error('Connection timeout'));
            }, 10000);

            this.ws.onopen = () => {
                clearTimeout(connectionTimeout);
                this.reconnectAttempts = 0;
                this.log('Connected to Digital Signer');
                this.onStatusChange?.('connected');
                resolve();
            };

            this.ws.onclose = (event) => {
                this.log(`Disconnected: ${event.code} ${event.reason}`);
                this.onStatusChange?.('disconnected');
                this.handleDisconnect();
            };

            this.ws.onerror = (error) => {
                clearTimeout(connectionTimeout);
                this.log(`WebSocket error: ${error}`);
                this.onError?.(error);
                reject(new Error('WebSocket connection failed. Is the Digital Signer app running?'));
            };

            this.ws.onmessage = (event) => {
                this.handleMessage(event.data);
            };
        });
    }

    disconnect() {
        this.reconnectAttempts = this.maxReconnectAttempts; // Prevent auto-reconnect
        this.ws?.close();
        this.ws = null;
    }

    async reconnect() {
        if (this.reconnectAttempts >= this.maxReconnectAttempts) {
            this.onError?.(new Error('Max reconnection attempts reached'));
            return false;
        }

        this.reconnectAttempts++;
        this.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
        
        await this.sleep(this.reconnectDelay * this.reconnectAttempts);
        
        try {
            await this.connect();
            return true;
        } catch {
            return false;
        }
    }

    handleDisconnect() {
        // Reject all pending requests
        for (const [requestId, pending] of this.pendingRequests) {
            pending.reject(new Error('Connection lost'));
        }
        this.pendingRequests.clear();
    }

    handleMessage(data) {
        try {
            const response = JSON.parse(data);
            const pending = this.pendingRequests.get(response.requestId);

            if (pending) {
                clearTimeout(pending.timeoutId);
                this.pendingRequests.delete(response.requestId);

                if (response.success) {
                    pending.resolve(response);
                } else {
                    const error = new Error(response.message || 'Operation failed');
                    error.code = response.errorCode;
                    pending.reject(error);
                }
            } else {
                // Welcome message or broadcast
                this.log(`Received: ${response.message || 'notification'}`);
            }
        } catch (error) {
            this.log(`Failed to parse message: ${error.message}`);
        }
    }

    sendRequest(request) {
        return new Promise((resolve, reject) => {
            if (!this.isConnected) {
                reject(new Error('Not connected to Digital Signer'));
                return;
            }

            const requestId = this.generateRequestId();
            request.requestId = requestId;

            const timeoutId = setTimeout(() => {
                if (this.pendingRequests.has(requestId)) {
                    this.pendingRequests.delete(requestId);
                    reject(new Error('Request timeout - user may have cancelled PIN entry'));
                }
            }, this.timeout);

            this.pendingRequests.set(requestId, { resolve, reject, timeoutId });

            try {
                this.ws.send(JSON.stringify(request));
                this.log(`Sent request: ${request.action}`);
            } catch (error) {
                clearTimeout(timeoutId);
                this.pendingRequests.delete(requestId);
                reject(error);
            }
        });
    }

    // API Methods

    async ping() {
        return this.sendRequest({ action: 'ping' });
    }

    async health() {
        return this.sendRequest({ action: 'health' });
    }

    async listCertificates() {
        return this.sendRequest({ action: 'list_certificates' });
    }

    async signPAdES(pdfBase64, options = {}) {
        return this.sendRequest({
            action: 'sign_pades',
            pdfBase64,
            reason: options.reason,
            location: options.location,
            certificateThumbprint: options.thumbprint,
            visibleSignature: options.visibleSignature || false,
            signaturePosition: options.signaturePosition,
            includeTimestamp: options.includeTimestamp || false,
            tsaUrl: options.tsaUrl
        });
    }

    async signCAdES(dataBase64, options = {}) {
        return this.sendRequest({
            action: 'sign_cades',
            dataBase64,
            certificateThumbprint: options.thumbprint
        });
    }

    async verifyCAdES(dataBase64, signatureBase64) {
        return this.sendRequest({
            action: 'verify_cades',
            dataBase64,
            pdfBase64: signatureBase64
        });
    }

    // Utility methods

    generateRequestId() {
        return 'req_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9);
    }

    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    log(message) {
        const timestamp = new Date().toISOString();
        console.log(`[DigitalSigner ${timestamp}] ${message}`);
        this.onLog?.(message);
    }

    // File helpers

    static async fileToBase64(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => {
                const result = reader.result;
                const base64 = result.split(',')[1];
                resolve(base64);
            };
            reader.onerror = () => reject(reader.error);
            reader.readAsDataURL(file);
        });
    }

    static downloadBase64(base64, filename, mimeType = 'application/pdf') {
        const link = document.createElement('a');
        link.href = `data:${mimeType};base64,${base64}`;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    static base64ToBlob(base64, mimeType = 'application/pdf') {
        const byteCharacters = atob(base64);
        const byteNumbers = new Array(byteCharacters.length);
        for (let i = 0; i < byteCharacters.length; i++) {
            byteNumbers[i] = byteCharacters.charCodeAt(i);
        }
        const byteArray = new Uint8Array(byteNumbers);
        return new Blob([byteArray], { type: mimeType });
    }
}

// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
    module.exports = DigitalSignerClient;
}
if (typeof window !== 'undefined') {
    window.DigitalSignerClient = DigitalSignerClient;
}

14. Build and Run Scripts

build.sh (macOS/Linux)

#!/bin/bash

# Build for current platform
dotnet build -c Release

# Publish for macOS
dotnet publish DigitalSignerApp.Desktop/DigitalSignerApp.Desktop.csproj \
    -c Release \
    -r osx-x64 \
    --self-contained true \
    -p:PublishSingleFile=true \
    -o ./publish/macos-x64

# Publish for macOS ARM
dotnet publish DigitalSignerApp.Desktop/DigitalSignerApp.Desktop.csproj \
    -c Release \
    -r osx-arm64 \
    --self-contained true \
    -p:PublishSingleFile=true \
    -o ./publish/macos-arm64

echo "Build complete!"

build.ps1 (Windows)

# Build for current platform
dotnet build -c Release

# Publish for Windows x64
dotnet publish DigitalSignerApp.Desktop/DigitalSignerApp.Desktop.csproj `
    -c Release `
    -r win-x64 `
    --self-contained true `
    -p:PublishSingleFile=true `
    -p:IncludeNativeLibrariesForSelfExtract=true `
    -o ./publish/windows-x64

Write-Host "Build complete!"

15. Key Improvements Summary

Category Improvement
Cross-Platform Uses Avalonia UI instead of WPF for Windows + macOS support
Architecture Dependency Injection with Microsoft.Extensions.DependencyInjection
Logging Serilog with file rolling and retention
Configuration JSON-based settings with automatic persistence
Error Handling Structured error codes and messages
Security WSS support for encrypted WebSocket connections
Certificate Access Platform-specific implementations (Windows Store + macOS Keychain)
MVVM CommunityToolkit.Mvvm for clean view/viewmodel separation
JSON System.Text.Json instead of Newtonsoft (better performance)
Single Instance Cross-platform mutex and named pipe implementation
Async/Await Proper async patterns throughout
Testability Interface-based services for easy mocking
User Experience Hardware token indicator, key type display
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment