Add WebAuthn event log reader project and Event Log tab in PasskeyUI

Introduce DSInternals.Win32.WebAuthn.Events library that reads and parses
events from the Microsoft-Windows-WebAuthN/Operational event log. Includes
typed event models for all known event IDs, CTAP2 CBOR request parsing
(via System.Formats.Cbor) for extracting user info, and aggregation of
related events into WebAuthnOperation objects grouped by transaction ID.

Add Event Log tab to PasskeyUI with a DataGrid displaying successful
registration and authentication operations with copiable fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Grafnetter
2026-03-10 14:44:20 +01:00
committed by Michael Grafnetter
co-authored by Claude Opus 4.6
parent 13dfdbf177
commit 3f6b70067d
30 changed files with 2135 additions and 0 deletions
@@ -0,0 +1,85 @@
using System;
using System.Formats.Cbor;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Parses CTAP2 authenticatorMakeCredential request CBOR to extract user information.
/// </summary>
internal static class CtapMakeCredentialRequestParser
{
// CTAP2 authenticatorMakeCredential parameter keys
private const int UserKey = 3;
/// <summary>
/// Extracts the user name and display name from a CTAP2 MakeCredential request CBOR blob.
/// </summary>
public static void ParseUserInfo(byte[]? request, out string? userName, out string? userDisplayName)
{
userName = null;
userDisplayName = null;
if (request == null || request.Length < 2)
return;
try
{
// Skip the first byte which is the CTAP2 command byte (0x01 for MakeCredential)
var cborData = new ReadOnlyMemory<byte>(request, 1, request.Length - 1);
var reader = new CborReader(cborData, CborConformanceMode.Lax);
int? mapLength = reader.ReadStartMap();
int count = mapLength ?? int.MaxValue;
for (int i = 0; i < count; i++)
{
if (reader.PeekState() == CborReaderState.EndMap)
break;
int key = reader.ReadInt32();
if (key == UserKey)
{
ReadUserMap(reader, out userName, out userDisplayName);
return;
}
reader.SkipValue();
}
}
catch (Exception)
{
// Silently ignore malformed CBOR
}
}
private static void ReadUserMap(CborReader reader, out string? userName, out string? userDisplayName)
{
userName = null;
userDisplayName = null;
int? mapLength = reader.ReadStartMap();
int count = mapLength ?? int.MaxValue;
for (int i = 0; i < count; i++)
{
if (reader.PeekState() == CborReaderState.EndMap)
break;
string fieldName = reader.ReadTextString();
switch (fieldName)
{
case "name":
userName = reader.ReadTextString();
break;
case "displayName":
userDisplayName = reader.ReadTextString();
break;
default:
reader.SkipValue();
break;
}
}
}
}
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>DSInternals.Win32.WebAuthn.Events</AssemblyName>
<RootNamespace>DSInternals.Win32.WebAuthn.Events</RootNamespace>
<PackageId>DSInternals.Win32.WebAuthn.Events</PackageId>
<Product>WebAuthn Event Log Reader</Product>
<Title>WebAuthn Event Log Reader</Title>
<Version>1.0.6</Version>
<AssemblyVersion>1.0.6</AssemblyVersion>
<ProductVersion>1.0.6</ProductVersion>
<FileVersion>1.0.6</FileVersion>
<Description>Reader for the Microsoft-Windows-WebAuthN/Operational event log with typed event models and WebAuthn operation aggregation.</Description>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' AND '$(Configuration)' == 'Release'">
<!-- Enable Source Link on CI builds -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<!-- DLLs need to be digitally signed first before a NuGet package is created. -->
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net48'">
<PackageReference Include="System.Diagnostics.EventLog" Version="9.0.3" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Formats.Cbor" Version="9.0.3" />
</ItemGroup>
<ItemGroup Condition="'$(GITHUB_ACTIONS)' == 'true' AND '$(Configuration)' == 'Release'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
</Project>
@@ -0,0 +1,30 @@
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents an API information query event (Event IDs 1070, 1071).
/// </summary>
/// <remarks>
/// <para>Sample event data for IsUserVerifyingPlatformAuthenticatorAvailable (1070):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="value"&gt;true&lt;/Data&gt;
/// &lt;Data Name="Error"&gt;0x0&lt;/Data&gt;
/// &lt;Data Name="HResult"&gt;0&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for ApiVersion (1071):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="value"&gt;9&lt;/Data&gt;
/// &lt;Data Name="Error"&gt;0x0&lt;/Data&gt;
/// &lt;Data Name="HResult"&gt;0&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class ApiInfoEvent : WebAuthnEvent
{
/// <summary>
/// The reported API value (e.g., "true" for authenticator availability, or the API version number).
/// </summary>
public string? Value { get; set; }
}
@@ -0,0 +1,72 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CTAP platform command event (Event IDs 2100, 2102, 2103).
/// </summary>
/// <remarks>
/// <para>Sample event data for CtapCommandStarted (2100):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Command"&gt;GetAllPlatformCredentials&lt;/Data&gt;
/// &lt;Data Name="TransactionId"&gt;{7177835f-89f9-4689-a7e9-839e62ce40bd}&lt;/Data&gt;
/// &lt;Data Name="Flags"&gt;0x0&lt;/Data&gt;
/// &lt;Data Name="TimeoutMilliseconds"&gt;0&lt;/Data&gt;
/// &lt;Data Name="TicketLength"&gt;0&lt;/Data&gt;
/// &lt;Data Name="Ticket"&gt;&lt;/Data&gt;
/// &lt;Data Name="RequestLength"&gt;201&lt;/Data&gt;
/// &lt;Data Name="Request"&gt;A967636F6D6D616E...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for CtapCommandCompleted (2102):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Command"&gt;GetAllPlatformCredentials&lt;/Data&gt;
/// &lt;Data Name="TransactionId"&gt;{7177835f-89f9-4689-a7e9-839e62ce40bd}&lt;/Data&gt;
/// &lt;Data Name="ResponseLength"&gt;29460&lt;/Data&gt;
/// &lt;Data Name="Response"&gt;8CAA000401582049...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for CtapCommandError (2103):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Command"&gt;MakeCredential&lt;/Data&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="Error"&gt;0x80090331&lt;/Data&gt;
/// &lt;Data Name="Win32Error"&gt;0x80090331&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class CtapCommandEvent : WebAuthnEvent
{
/// <summary>
/// The CTAP command name or identifier.
/// </summary>
public int? Command { get; set; }
/// <summary>
/// Command flags.
/// </summary>
public int? Flags { get; set; }
/// <summary>
/// Command timeout in milliseconds.
/// </summary>
public int? TimeoutMilliseconds { get; set; }
/// <summary>
/// Authentication ticket blob.
/// </summary>
public byte[]? Ticket { get; set; }
/// <summary>
/// The CBOR-encoded request blob.
/// </summary>
public byte[]? Request { get; set; }
/// <summary>
/// The CBOR-encoded response blob.
/// </summary>
public byte[]? Response { get; set; }
}
@@ -0,0 +1,53 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CTAP device info event (Event ID 2104).
/// </summary>
/// <remarks>
/// <para>Sample event data:</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{5ec89aa0-49a3-46f5-a486-b77df0a7555e}&lt;/Data&gt;
/// &lt;Data Name="ProviderName"&gt;MicrosoftPlatformProvider&lt;/Data&gt;
/// &lt;Data Name="DevicePath"&gt;&lt;/Data&gt;
/// &lt;Data Name="Manufacturer"&gt;&lt;/Data&gt;
/// &lt;Data Name="Product"&gt;&lt;/Data&gt;
/// &lt;Data Name="AAGuid"&gt;{00000000-0000-0000-0000-000000000000}&lt;/Data&gt;
/// &lt;Data Name="U2fProtocol"&gt;false&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class DeviceInfoEvent : WebAuthnEvent
{
/// <summary>
/// The name of the CTAP provider (e.g., MicrosoftPlatformProvider).
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// The HID device path of the authenticator.
/// </summary>
public string? DevicePath { get; set; }
/// <summary>
/// The manufacturer of the authenticator device.
/// </summary>
public string? Manufacturer { get; set; }
/// <summary>
/// The product name of the authenticator device.
/// </summary>
public string? Product { get; set; }
/// <summary>
/// Authenticator Attestation GUID identifying the authenticator model.
/// </summary>
public Guid? AAGuid { get; set; }
/// <summary>
/// Whether the device uses the legacy U2F protocol.
/// </summary>
public bool? U2fProtocol { get; set; }
}
@@ -0,0 +1,37 @@
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CTAP function warning event (Event IDs 2105, 2270).
/// </summary>
/// <remarks>
/// <para>Sample event data for FunctionWarning (2105):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Function"&gt;HidSilentGetAssertion&lt;/Data&gt;
/// &lt;Data Name="Location"&gt;SendRequest&lt;/Data&gt;
/// &lt;Data Name="Error"&gt;0x8009030d&lt;/Data&gt;
/// &lt;Data Name="Win32Error"&gt;0x8009030d&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for BleFunctionWarning (2270):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Function"&gt;BleGetAssertion&lt;/Data&gt;
/// &lt;Data Name="Location"&gt;ConnectDevice&lt;/Data&gt;
/// &lt;Data Name="Error"&gt;0x80070005&lt;/Data&gt;
/// &lt;Data Name="Win32Error"&gt;0x80070005&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class FunctionWarningEvent : WebAuthnEvent
{
/// <summary>
/// The name of the function that generated the warning.
/// </summary>
public string? Function { get; set; }
/// <summary>
/// The location within the function where the warning occurred.
/// </summary>
public string? Location { get; set; }
}
@@ -0,0 +1,55 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CBOR-encoded GetAssertion request event (Event ID 1103).
/// </summary>
/// <remarks>
/// <para>Sample event data:</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{5ec89aa0-49a3-46f5-a486-b77df0a7555e}&lt;/Data&gt;
/// &lt;Data Name="RpId"&gt;login.microsoft.com&lt;/Data&gt;
/// &lt;Data Name="ClientDataHashAlgId"&gt;SHA-256&lt;/Data&gt;
/// &lt;Data Name="ClientDataLength"&gt;131&lt;/Data&gt;
/// &lt;Data Name="ClientDataHashLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="ClientDataHash"&gt;465746E9120D0C16...&lt;/Data&gt;
/// &lt;Data Name="CredentialCount"&gt;5&lt;/Data&gt;
/// &lt;Data Name="RequestLength"&gt;420&lt;/Data&gt;
/// &lt;Data Name="Request"&gt;02A4016A6C6F6769...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class GetAssertionRequestEvent : WebAuthnEvent
{
/// <summary>
/// The relying party identifier.
/// </summary>
public string? RpId { get; set; }
/// <summary>
/// The hash algorithm used for client data (e.g., SHA-256).
/// </summary>
public string? ClientDataHashAlgId { get; set; }
/// <summary>
/// The length of the client data in bytes.
/// </summary>
public int? ClientDataLength { get; set; }
/// <summary>
/// The hash of the client data.
/// </summary>
public byte[]? ClientDataHash { get; set; }
/// <summary>
/// Number of credentials in the allow list.
/// </summary>
public int? CredentialCount { get; set; }
/// <summary>
/// The raw CTAP2 CBOR-encoded request blob (prefixed with command byte 0x02).
/// </summary>
public byte[]? Request { get; set; }
}
@@ -0,0 +1,50 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CBOR-decoded GetAssertion response event (Event ID 1104).
/// </summary>
/// <remarks>
/// <para>Sample event data:</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{5ec89aa0-49a3-46f5-a486-b77df0a7555e}&lt;/Data&gt;
/// &lt;Data Name="RpIdHashLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="RpIdHash"&gt;3AEB002460381C6F...&lt;/Data&gt;
/// &lt;Data Name="Flags"&gt;0x5&lt;/Data&gt;
/// &lt;Data Name="SignCount"&gt;0x54&lt;/Data&gt;
/// &lt;Data Name="CredentialIdLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="CredentialId"&gt;90613158E4C99370...&lt;/Data&gt;
/// &lt;Data Name="ResponseLength"&gt;240&lt;/Data&gt;
/// &lt;Data Name="Response"&gt;A401A26269645820...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class GetAssertionResponseEvent : WebAuthnEvent
{
/// <summary>
/// SHA-256 hash of the relying party ID.
/// </summary>
public byte[]? RpIdHash { get; set; }
/// <summary>
/// Authenticator data flags byte (UP, UV, AT, ED, etc.).
/// </summary>
public byte? AuthenticatorFlags { get; set; }
/// <summary>
/// Signature counter value.
/// </summary>
public uint? SignCount { get; set; }
/// <summary>
/// The credential ID used for the assertion.
/// </summary>
public byte[]? CredentialId { get; set; }
/// <summary>
/// The raw CBOR-encoded response blob.
/// </summary>
public byte[]? Response { get; set; }
}
@@ -0,0 +1,74 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CBOR-encoded MakeCredential request event (Event ID 1101).
/// </summary>
/// <remarks>
/// <para>Sample event data:</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="RpId"&gt;login.microsoft.com&lt;/Data&gt;
/// &lt;Data Name="UserIdLength"&gt;16&lt;/Data&gt;
/// &lt;Data Name="UserId"&gt;A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4&lt;/Data&gt;
/// &lt;Data Name="ClientDataHashAlgId"&gt;SHA-256&lt;/Data&gt;
/// &lt;Data Name="ClientDataLength"&gt;210&lt;/Data&gt;
/// &lt;Data Name="ClientDataHashLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="ClientDataHash"&gt;465746E9120D0C16...&lt;/Data&gt;
/// &lt;Data Name="RequireResidentKey"&gt;true&lt;/Data&gt;
/// &lt;Data Name="CredentialCount"&gt;0&lt;/Data&gt;
/// &lt;Data Name="CredentialParameterCount"&gt;2&lt;/Data&gt;
/// &lt;Data Name="RequestLength"&gt;181&lt;/Data&gt;
/// &lt;Data Name="Request"&gt;01A501786C6F67696E...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class MakeCredentialRequestEvent : WebAuthnEvent
{
/// <summary>
/// The relying party identifier.
/// </summary>
public string? RpId { get; set; }
/// <summary>
/// The user identifier provided by the relying party.
/// </summary>
public byte[]? UserId { get; set; }
/// <summary>
/// The hash algorithm used for client data (e.g., SHA-256).
/// </summary>
public string? ClientDataHashAlgId { get; set; }
/// <summary>
/// The length of the client data in bytes.
/// </summary>
public int? ClientDataLength { get; set; }
/// <summary>
/// The hash of the client data.
/// </summary>
public byte[]? ClientDataHash { get; set; }
/// <summary>
/// Whether a resident (discoverable) credential was required.
/// </summary>
public bool? RequireResidentKey { get; set; }
/// <summary>
/// Number of credentials in the exclude list.
/// </summary>
public int? CredentialCount { get; set; }
/// <summary>
/// The number of credential parameter types requested.
/// </summary>
public int? CredentialParameterCount { get; set; }
/// <summary>
/// The raw CTAP2 CBOR-encoded request blob (prefixed with command byte 0x01).
/// </summary>
public byte[]? Request { get; set; }
}
@@ -0,0 +1,94 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CBOR-decoded MakeCredential response event (Event IDs 1102, 1041).
/// </summary>
/// <remarks>
/// <para>Sample event data for CborMakeCredentialResponse (1102):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="AttestationFormatType"&gt;none&lt;/Data&gt;
/// &lt;Data Name="RpIdHashLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="RpIdHash"&gt;3AEB002460381C6F...&lt;/Data&gt;
/// &lt;Data Name="Flags"&gt;0x45&lt;/Data&gt;
/// &lt;Data Name="SignCount"&gt;0x0&lt;/Data&gt;
/// &lt;Data Name="AAGuid"&gt;{de1e552d-db1d-4423-a439-09523d6f04a0}&lt;/Data&gt;
/// &lt;Data Name="CredentialIdLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="CredentialId"&gt;90613158E4C99370...&lt;/Data&gt;
/// &lt;Data Name="U2fPublicKey"&gt;&lt;/Data&gt;
/// &lt;Data Name="PublicKeyLength"&gt;77&lt;/Data&gt;
/// &lt;Data Name="PublicKey"&gt;A5010203262001...&lt;/Data&gt;
/// &lt;Data Name="ResponseLength"&gt;240&lt;/Data&gt;
/// &lt;Data Name="Response"&gt;A401A26269645820...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for NgcMakeCredentialResponse (1041):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="AttestationFormatType"&gt;packed&lt;/Data&gt;
/// &lt;Data Name="RpIdHashLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="RpIdHash"&gt;49960DE5880E8C68...&lt;/Data&gt;
/// &lt;Data Name="Flags"&gt;0x45&lt;/Data&gt;
/// &lt;Data Name="SignCount"&gt;0x0&lt;/Data&gt;
/// &lt;Data Name="AAGuid"&gt;{de1e552d-db1d-4423-a439-09523d6f04a0}&lt;/Data&gt;
/// &lt;Data Name="CredentialIdLength"&gt;32&lt;/Data&gt;
/// &lt;Data Name="CredentialId"&gt;F2B3A1D4E5C67890...&lt;/Data&gt;
/// &lt;Data Name="U2fPublicKey"&gt;&lt;/Data&gt;
/// &lt;Data Name="PublicKeyLength"&gt;77&lt;/Data&gt;
/// &lt;Data Name="PublicKey"&gt;A5010203262001...&lt;/Data&gt;
/// &lt;Data Name="ResponseLength"&gt;350&lt;/Data&gt;
/// &lt;Data Name="Response"&gt;A301667061636B65...&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class MakeCredentialResponseEvent : WebAuthnEvent
{
/// <summary>
/// The attestation format type (e.g., "none", "packed", "tpm").
/// </summary>
public string? AttestationFormatType { get; set; }
/// <summary>
/// SHA-256 hash of the relying party ID.
/// </summary>
public byte[]? RpIdHash { get; set; }
/// <summary>
/// Authenticator data flags byte (UP, UV, AT, ED, etc.).
/// </summary>
public byte? AuthenticatorFlags { get; set; }
/// <summary>
/// Signature counter value.
/// </summary>
public uint? SignCount { get; set; }
/// <summary>
/// Authenticator Attestation GUID identifying the authenticator model.
/// </summary>
public Guid? AAGuid { get; set; }
/// <summary>
/// The credential ID of the newly created credential.
/// </summary>
public byte[]? CredentialId { get; set; }
/// <summary>
/// The U2F public key, if the authenticator uses the legacy U2F protocol.
/// </summary>
public byte[]? U2fPublicKey { get; set; }
/// <summary>
/// The COSE-encoded public key of the newly created credential.
/// </summary>
public byte[]? PublicKey { get; set; }
/// <summary>
/// The raw CBOR-encoded response blob.
/// </summary>
public byte[]? Response { get; set; }
}
@@ -0,0 +1,27 @@
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a CTAP name-value pair event (Event ID 2106).
/// Contains JSON response data like authenticationResponseJSON or registrationResponseJSON.
/// </summary>
/// <remarks>
/// <para>Sample event data:</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="Name"&gt;authenticationResponseJSON&lt;/Data&gt;
/// &lt;Data Name="Value"&gt;{"authenticatorAttachment":"platform","clientExtensionResults":{},...}&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class NameValueEvent : WebAuthnEvent
{
/// <summary>
/// The name of the value (e.g., authenticationResponseJSON, registrationResponseJSON).
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The value, typically a JSON string containing the WebAuthn response.
/// </summary>
public string? Value { get; set; }
}
@@ -0,0 +1,65 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Represents a USB device event (Event IDs 2210, 2211, 2220).
/// </summary>
/// <remarks>
/// <para>Sample event data for UsbDeviceStarted (2210):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="DevicePath"&gt;\\?\hid#vid_1050&amp;pid_0407#...&lt;/Data&gt;
/// &lt;Data Name="Manufacturer"&gt;Yubico&lt;/Data&gt;
/// &lt;Data Name="Product"&gt;YubiKey OTP+FIDO+CCID&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for UsbDeviceCompleted (2211):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="DevicePath"&gt;\\?\hid#vid_1050&amp;pid_0407#...&lt;/Data&gt;
/// &lt;Data Name="Manufacturer"&gt;Yubico&lt;/Data&gt;
/// &lt;Data Name="Product"&gt;YubiKey OTP+FIDO+CCID&lt;/Data&gt;
/// &lt;Data Name="AAGuid"&gt;{2fc0579f-8113-47ea-b116-bb5a8db9202a}&lt;/Data&gt;
/// &lt;Data Name="U2fProtocol"&gt;false&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// <para>Sample event data for UsbAddDevice (2220):</para>
/// <code>
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;Data Name="DevicePath"&gt;\\?\hid#vid_1050&amp;pid_0407#...&lt;/Data&gt;
/// &lt;Data Name="Manufacturer"&gt;Yubico&lt;/Data&gt;
/// &lt;Data Name="Product"&gt;YubiKey OTP+FIDO+CCID&lt;/Data&gt;
/// &lt;/EventData&gt;
/// </code>
/// </remarks>
public sealed class UsbDeviceEvent : WebAuthnEvent
{
/// <summary>
/// The HID device path of the USB authenticator.
/// </summary>
public string? DevicePath { get; set; }
/// <summary>
/// The manufacturer of the USB authenticator device.
/// </summary>
public string? Manufacturer { get; set; }
/// <summary>
/// The product name of the USB authenticator device.
/// </summary>
public string? Product { get; set; }
/// <summary>
/// AAGUID of the USB authenticator. Only available in completed events (2211).
/// </summary>
public Guid? AAGuid { get; set; }
/// <summary>
/// Whether the device uses the U2F protocol. Only available in completed events (2211).
/// </summary>
public bool? U2fProtocol { get; set; }
}
@@ -0,0 +1,73 @@
using System;
using System.Diagnostics.Eventing.Reader;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Base class for all WebAuthN event log entries from the
/// <c>Microsoft-Windows-WebAuthN/Operational</c> event log.
/// </summary>
/// <remarks>
/// <para>Sample event XML:</para>
/// <code>
/// &lt;Event&gt;
/// &lt;System&gt;
/// &lt;Provider Name="Microsoft-Windows-WebAuthN" Guid="{3ae1ea61-c002-47fb-b06c-4022a8c98929}" /&gt;
/// &lt;EventID&gt;1003&lt;/EventID&gt;
/// &lt;Level&gt;4&lt;/Level&gt;
/// &lt;TimeCreated SystemTime="2026-03-09T15:07:24.2720199Z" /&gt;
/// &lt;Execution ProcessID="20656" ThreadID="21180" /&gt;
/// &lt;/System&gt;
/// &lt;EventData&gt;
/// &lt;Data Name="TransactionId"&gt;{aabbccdd-1234-5678-9abc-def012345678}&lt;/Data&gt;
/// &lt;/EventData&gt;
/// &lt;/Event&gt;
/// </code>
/// </remarks>
public class WebAuthnEvent
{
/// <summary>
/// The event ID identifying the event type.
/// </summary>
public WebAuthnEventId EventId { get; set; }
/// <summary>
/// The time the event was created.
/// </summary>
public DateTime? TimeCreated { get; set; }
/// <summary>
/// The process ID that generated the event.
/// </summary>
public int ProcessId { get; set; }
/// <summary>
/// The thread ID that generated the event.
/// </summary>
public int ThreadId { get; set; }
/// <summary>
/// The severity level of the event.
/// </summary>
public StandardEventLevel? Level { get; set; }
/// <summary>
/// The full event message text.
/// </summary>
public string? Message { get; set; }
/// <summary>
/// The transaction ID correlating related events. May be null for events like API info queries.
/// </summary>
public Guid? TransactionId { get; set; }
/// <summary>
/// Error description, if present.
/// </summary>
public string? Error { get; set; }
/// <summary>
/// HRESULT or Win32 error code, if present.
/// </summary>
public int? HResult { get; set; }
}
@@ -0,0 +1,93 @@
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Known event IDs from the Microsoft-Windows-WebAuthN/Operational event log.
/// </summary>
public enum WebAuthnEventId
{
// MakeCredential lifecycle
MakeCredentialStarted = 1000,
MakeCredentialCompleted = 1001,
MakeCredentialError = 1002,
// GetAssertion lifecycle
GetAssertionStarted = 1003,
GetAssertionCompleted = 1004,
GetAssertionError = 1005,
// SendCommand lifecycle
SendCommandStarted = 1006,
SendCommandCompleted = 1007,
SendCommandError = 1008,
// NGC MakeCredential lifecycle
NgcMakeCredentialStarted = 1020,
NgcMakeCredentialCompleted = 1021,
NgcMakeCredentialError = 1022,
// NGC GetAssertion lifecycle
NgcGetAssertionStarted = 1023,
NgcGetAssertionCompleted = 1024,
NgcGetAssertionError = 1025,
// NGC MakeCredential response
NgcMakeCredentialResponse = 1041,
// Remote RPC
RemoteRpcRequest = 1050,
RemoteRpcResponse = 1052,
// API information
IsUserVerifyingPlatformAuthenticatorAvailable = 1070,
ApiVersion = 1071,
// CBOR encode/decode
CborMakeCredentialRequest = 1101,
CborMakeCredentialResponse = 1102,
CborGetAssertionRequest = 1103,
CborGetAssertionResponse = 1104,
// Service lifecycle
ServiceStarted = 2000,
ServiceStopped = 2001,
// CTAP platform operations
CtapCommandStarted = 2100,
CtapCommandCompleted = 2102,
CtapCommandError = 2103,
// Device info
DeviceInfo = 2104,
FunctionWarning = 2105,
NameValue = 2106,
DeviceStateInfo = 2110,
// USB transport
UsbProviderStarted = 2200,
UsbProviderCompleted = 2201,
UsbProviderError = 2202,
UsbProviderWarning = 2203,
UsbDeviceStarted = 2210,
UsbDeviceCompleted = 2211,
UsbAddDevice = 2220,
UsbDeviceChanges = 2222,
UsbSendReceive = 2225,
// BLE transport
BleProviderStarted = 2250,
BleProviderWarning = 2253,
BleFunctionWarning = 2270,
// NFC transport
NfcProviderStarted = 2300,
NfcProviderWarning = 2303,
NfcSkipReader = 2321,
// Hybrid transport
HybridProcessStarted = 2329,
HybridProcessError = 2331,
HybridWriteMessage = 2332,
HybridReadMessage = 2333,
HybridSetupStarted = 2334,
HybridSetupCompleted = 2335
}
@@ -0,0 +1,15 @@
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Aggregated data about a WebAuthn authentication (GetAssertion) operation,
/// combining information from multiple related WebAuthN events.
/// </summary>
public sealed class WebAuthnAssertionOperation : WebAuthnOperation
{
public override string OperationType => "Authentication";
/// <summary>
/// The hash algorithm used for client data (e.g., SHA-256).
/// </summary>
public string? ClientDataHashAlgId { get; set; }
}
@@ -0,0 +1,42 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Aggregated data about a WebAuthn registration (MakeCredential) operation,
/// combining information from multiple related WebAuthN events.
/// </summary>
public sealed class WebAuthnAttestationOperation : WebAuthnOperation
{
public override string OperationType => "Registration";
/// <summary>
/// Whether a resident (discoverable) key was required.
/// </summary>
public bool? RequireResidentKey { get; set; }
/// <summary>
/// The number of credential parameter types requested.
/// </summary>
public int? CredentialParameterCount { get; set; }
/// <summary>
/// The attestation format type (e.g., "none", "packed", "tpm").
/// </summary>
public string? AttestationFormatType { get; set; }
/// <summary>
/// The hash algorithm used for client data (e.g., SHA-256).
/// </summary>
public string? ClientDataHashAlgId { get; set; }
/// <summary>
/// The U2F public key, if available.
/// </summary>
public byte[]? U2fPublicKey { get; set; }
/// <summary>
/// The COSE-encoded public key.
/// </summary>
public byte[]? PublicKey { get; set; }
}
@@ -0,0 +1,600 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Globalization;
using System.Linq;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Reads events from the Microsoft-Windows-WebAuthN/Operational event log.
/// </summary>
public static class WebAuthnEventReader
{
private const string LogName = "Microsoft-Windows-WebAuthN/Operational";
/// <summary>
/// Reads all events from the local WebAuthN event log.
/// </summary>
public static IReadOnlyList<WebAuthnEvent> ReadEvents(int? maxEvents = null)
{
return ReadEvents(LogName, PathType.LogName, maxEvents);
}
/// <summary>
/// Reads events from a saved .evtx file.
/// </summary>
public static IReadOnlyList<WebAuthnEvent> ReadEventsFromFile(string filePath, int? maxEvents = null)
{
return ReadEvents(filePath, PathType.FilePath, maxEvents);
}
private static IReadOnlyList<WebAuthnEvent> ReadEvents(string path, PathType pathType, int? maxEvents)
{
var events = new List<WebAuthnEvent>();
using var logReader = new EventLogReader(new EventLogQuery(path, pathType) { ReverseDirection = true });
EventRecord? record;
int count = 0;
while ((record = logReader.ReadEvent()) != null)
{
using (record)
{
var evt = ParseEvent(record);
if (evt != null)
{
events.Add(evt);
count++;
if (maxEvents.HasValue && count >= maxEvents.Value)
{
break;
}
}
}
}
return events;
}
private static WebAuthnEvent? ParseEvent(EventRecord record)
{
if (record.Id == 0)
{
return null;
}
var eventId = (WebAuthnEventId)record.Id;
var properties = GetEventProperties(record);
string? message = null;
try
{
message = record.FormatDescription();
}
catch
{
// FormatDescription can throw if the event provider is not registered
}
return eventId switch
{
WebAuthnEventId.CborMakeCredentialRequest =>
ParseMakeCredentialRequest(record, properties, eventId, message),
WebAuthnEventId.CborMakeCredentialResponse or WebAuthnEventId.NgcMakeCredentialResponse =>
ParseMakeCredentialResponse(record, properties, eventId, message),
WebAuthnEventId.CborGetAssertionRequest =>
ParseGetAssertionRequest(record, properties, eventId, message),
WebAuthnEventId.CborGetAssertionResponse =>
ParseGetAssertionResponse(record, properties, eventId, message),
WebAuthnEventId.DeviceInfo =>
ParseDeviceInfo(record, properties, eventId, message),
WebAuthnEventId.UsbDeviceCompleted =>
ParseUsbDeviceCompleted(record, properties, eventId, message),
WebAuthnEventId.UsbDeviceStarted or WebAuthnEventId.UsbAddDevice =>
ParseUsbDeviceStarted(record, properties, eventId, message),
WebAuthnEventId.IsUserVerifyingPlatformAuthenticatorAvailable or WebAuthnEventId.ApiVersion =>
ParseApiInfo(record, properties, eventId, message),
WebAuthnEventId.NameValue =>
ParseNameValue(record, properties, eventId, message),
WebAuthnEventId.CtapCommandStarted =>
ParseCtapCommandStarted(record, properties, eventId, message),
WebAuthnEventId.CtapCommandCompleted =>
ParseCtapCommandCompleted(record, properties, eventId, message),
WebAuthnEventId.CtapCommandError =>
ParseCtapCommandError(record, properties, eventId, message),
WebAuthnEventId.FunctionWarning or WebAuthnEventId.BleFunctionWarning =>
ParseFunctionWarning(record, properties, eventId, message),
_ => ParseBaseEvent(record, properties, eventId, message)
};
}
private static StandardEventLevel? GetLevel(EventRecord record)
{
return record.Level.HasValue ? (StandardEventLevel)record.Level.Value : null;
}
private static WebAuthnEvent ParseBaseEvent(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
Guid? transactionId = null;
string? error = null;
int? hResult = null;
// Determine the field layout based on event ID
switch (eventId)
{
// Events with TransactionId only
case WebAuthnEventId.MakeCredentialStarted:
case WebAuthnEventId.MakeCredentialCompleted:
case WebAuthnEventId.GetAssertionStarted:
case WebAuthnEventId.GetAssertionCompleted:
case WebAuthnEventId.SendCommandCompleted:
case WebAuthnEventId.NgcMakeCredentialStarted:
case WebAuthnEventId.NgcMakeCredentialCompleted:
case WebAuthnEventId.NgcGetAssertionStarted:
case WebAuthnEventId.NgcGetAssertionCompleted:
case WebAuthnEventId.UsbProviderStarted:
case WebAuthnEventId.UsbProviderCompleted:
case WebAuthnEventId.UsbDeviceChanges:
case WebAuthnEventId.BleProviderStarted:
case WebAuthnEventId.NfcProviderStarted:
case WebAuthnEventId.HybridProcessStarted:
case WebAuthnEventId.HybridSetupStarted:
if (properties.Count > 0)
{
transactionId = GetGuid(properties, 0);
}
break;
// Events with TransactionId + Error + HResult
case WebAuthnEventId.MakeCredentialError:
case WebAuthnEventId.GetAssertionError:
case WebAuthnEventId.SendCommandError:
case WebAuthnEventId.NgcMakeCredentialError:
case WebAuthnEventId.NgcGetAssertionError:
case WebAuthnEventId.HybridProcessError:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
if (properties.Count > 1) error = GetString(properties, 1);
if (properties.Count > 2) hResult = GetInt32(properties, 2);
break;
// Events with TransactionId + Error + Win32Error
case WebAuthnEventId.UsbProviderError:
case WebAuthnEventId.UsbProviderWarning:
case WebAuthnEventId.BleProviderWarning:
case WebAuthnEventId.NfcProviderWarning:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
if (properties.Count > 1) error = GetString(properties, 1);
if (properties.Count > 2) hResult = GetInt32(properties, 2);
break;
// SendCommand started: TransactionId, TicketLength, Ticket
case WebAuthnEventId.SendCommandStarted:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
break;
// Hybrid setup completed: TransactionId, Action, Error, HResult
case WebAuthnEventId.HybridSetupCompleted:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
if (properties.Count > 2) error = GetString(properties, 2);
if (properties.Count > 3) hResult = GetInt32(properties, 3);
break;
// NFC skip reader: TransactionId, Action, Reader, DeviceInstanceId
case WebAuthnEventId.NfcSkipReader:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
break;
// Remote RPC request: TransactionId, RemoteRpcRequestLength, RemoteRpcRequest
case WebAuthnEventId.RemoteRpcRequest:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
break;
// Remote RPC response: TransactionId, Error, HResult, ...
case WebAuthnEventId.RemoteRpcResponse:
if (properties.Count > 0) transactionId = GetGuid(properties, 0);
if (properties.Count > 1) error = GetString(properties, 1);
if (properties.Count > 2) hResult = GetInt32(properties, 2);
break;
// Device state info: Transport, WnfState, Error, Win32Error
case WebAuthnEventId.DeviceStateInfo:
if (properties.Count > 2) error = GetString(properties, 2);
if (properties.Count > 3) hResult = GetInt32(properties, 3);
break;
}
return new WebAuthnEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = transactionId,
Error = error,
HResult = hResult
};
}
private static MakeCredentialRequestEvent ParseMakeCredentialRequest(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, RpId, UserIdLength, UserId, ClientDataHashAlgId, ClientDataLength, ClientDataHashLength, ClientDataHash, RequireResidentKey, CredentialCount, CredentialParameterCount, RequestLength, Request
return new MakeCredentialRequestEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
RpId = GetString(properties, 1),
UserId = GetByteArray(properties, 3), // index 2 is UserIdLength
ClientDataHashAlgId = GetString(properties, 4),
ClientDataLength = GetInt32(properties, 5),
ClientDataHash = GetByteArray(properties, 7), // index 6 is ClientDataHashLength
RequireResidentKey = GetBoolean(properties, 8),
CredentialCount = GetInt32(properties, 9),
CredentialParameterCount = GetInt32(properties, 10),
Request = GetByteArray(properties, 12) // index 11 is RequestLength
};
}
private static MakeCredentialResponseEvent ParseMakeCredentialResponse(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, AttestationFormatType, RpIdHashLength, RpIdHash, Flags, SignCount, AAGuid, CredentialIdLength, CredentialId, U2fPublicKey, PublicKeyLength, PublicKey, ResponseLength, Response
return new MakeCredentialResponseEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
AttestationFormatType = GetString(properties, 1),
RpIdHash = GetByteArray(properties, 3), // index 2 is RpIdHashLength
AuthenticatorFlags = GetByte(properties, 4),
SignCount = GetUInt32(properties, 5),
AAGuid = GetGuid(properties, 6),
CredentialId = GetByteArray(properties, 8), // index 7 is CredentialIdLength
U2fPublicKey = GetByteArray(properties, 9),
PublicKey = GetByteArray(properties, 11), // index 10 is PublicKeyLength
Response = GetByteArray(properties, 13) // index 12 is ResponseLength
};
}
private static GetAssertionRequestEvent ParseGetAssertionRequest(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, RpId, ClientDataHashAlgId, ClientDataLength, ClientDataHashLength, ClientDataHash, CredentialCount, RequestLength, Request
return new GetAssertionRequestEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
RpId = GetString(properties, 1),
ClientDataHashAlgId = GetString(properties, 2),
ClientDataLength = GetInt32(properties, 3),
ClientDataHash = GetByteArray(properties, 5), // index 4 is ClientDataHashLength
CredentialCount = GetInt32(properties, 6),
Request = GetByteArray(properties, 8) // index 7 is RequestLength
};
}
private static GetAssertionResponseEvent ParseGetAssertionResponse(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, RpIdHashLength, RpIdHash, Flags, SignCount, CredentialIdLength, CredentialId, ResponseLength, Response
return new GetAssertionResponseEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
RpIdHash = GetByteArray(properties, 2), // index 1 is RpIdHashLength
AuthenticatorFlags = GetByte(properties, 3),
SignCount = GetUInt32(properties, 4),
CredentialId = GetByteArray(properties, 6), // index 5 is CredentialIdLength
Response = GetByteArray(properties, 8) // index 7 is ResponseLength
};
}
private static DeviceInfoEvent ParseDeviceInfo(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, ProviderName, DevicePath, Manufacturer, Product, AAGuid, U2fProtocol
return new DeviceInfoEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
ProviderName = GetString(properties, 1),
DevicePath = GetString(properties, 2),
Manufacturer = GetString(properties, 3),
Product = GetString(properties, 4),
AAGuid = GetGuid(properties, 5),
U2fProtocol = GetBoolean(properties, 6)
};
}
private static UsbDeviceEvent ParseUsbDeviceCompleted(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, DevicePath, Manufacturer, Product, AAGuid, U2fProtocol
return new UsbDeviceEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
DevicePath = GetString(properties, 1),
Manufacturer = GetString(properties, 2),
Product = GetString(properties, 3),
AAGuid = GetGuid(properties, 4),
U2fProtocol = GetBoolean(properties, 5)
};
}
private static UsbDeviceEvent ParseUsbDeviceStarted(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: TransactionId, DevicePath, Manufacturer, Product
return new UsbDeviceEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
TransactionId = GetGuid(properties, 0),
DevicePath = GetString(properties, 1),
Manufacturer = GetString(properties, 2),
Product = GetString(properties, 3)
};
}
private static ApiInfoEvent ParseApiInfo(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: value, Error, HResult
return new ApiInfoEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Value = GetString(properties, 0),
Error = GetString(properties, 1),
HResult = GetInt32(properties, 2)
};
}
private static NameValueEvent ParseNameValue(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: Name, Value
return new NameValueEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Name = GetString(properties, 0),
Value = GetString(properties, 1)
};
}
private static CtapCommandEvent ParseCtapCommandStarted(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: Command, TransactionId, Flags, TimeoutMilliseconds, TicketLength, Ticket, RequestLength, Request
return new CtapCommandEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Command = GetInt32(properties, 0),
TransactionId = GetGuid(properties, 1),
Flags = GetInt32(properties, 2),
TimeoutMilliseconds = GetInt32(properties, 3),
Ticket = GetByteArray(properties, 5), // index 4 is TicketLength
Request = GetByteArray(properties, 7) // index 6 is RequestLength
};
}
private static CtapCommandEvent ParseCtapCommandCompleted(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: Command, TransactionId, ResponseLength, Response
return new CtapCommandEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Command = GetInt32(properties, 0),
TransactionId = GetGuid(properties, 1),
Response = GetByteArray(properties, 3) // index 2 is ResponseLength
};
}
private static CtapCommandEvent ParseCtapCommandError(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: Command, TransactionId, Error, Win32Error
return new CtapCommandEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Command = GetInt32(properties, 0),
TransactionId = GetGuid(properties, 1),
Error = GetString(properties, 2),
HResult = GetInt32(properties, 3)
};
}
private static FunctionWarningEvent ParseFunctionWarning(EventRecord record, IList<EventProperty> properties, WebAuthnEventId eventId, string? message)
{
// Fields: Function, Location, Error, Win32Error
return new FunctionWarningEvent
{
EventId = eventId,
TimeCreated = record.TimeCreated?.ToLocalTime(),
ProcessId = (int)(record.ProcessId ?? 0),
ThreadId = (int)(record.ThreadId ?? 0),
Level = GetLevel(record),
Message = message,
Function = GetString(properties, 0),
Location = GetString(properties, 1),
Error = GetString(properties, 2),
HResult = GetInt32(properties, 3)
};
}
#region Property extraction helpers
private static IList<EventProperty> GetEventProperties(EventRecord record)
{
try
{
return record.Properties;
}
catch
{
return Array.Empty<EventProperty>();
}
}
private static string? GetString(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value?.ToString();
}
private static int? GetInt32(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
int i => i,
uint u => unchecked((int)u),
long l => (int)l,
ulong ul => (int)ul,
string s when int.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out int parsed) => parsed,
_ => null
};
}
private static uint? GetUInt32(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
uint u => u,
int i => unchecked((uint)i),
long l => (uint)l,
ulong ul => (uint)ul,
string s when uint.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out uint parsed) => parsed,
_ => null
};
}
private static byte? GetByte(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
byte b => b,
int i => (byte)i,
uint u => (byte)u,
long l => (byte)l,
_ => null
};
}
private static bool? GetBoolean(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
bool b => b,
string s => bool.TryParse(s, out bool parsed) ? parsed : null,
int i => i != 0,
_ => null
};
}
private static Guid? GetGuid(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
Guid g => g == Guid.Empty ? null : g,
string s when Guid.TryParse(s, out Guid parsed) => parsed == Guid.Empty ? null : parsed,
_ => null
};
}
private static byte[]? GetByteArray(IList<EventProperty> properties, int index)
{
if (index >= properties.Count) return null;
var value = properties[index].Value;
return value switch
{
byte[] bytes when bytes.Length > 0 => bytes,
byte[] => null,
_ => null
};
}
#endregion
}
@@ -0,0 +1,140 @@
using System;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Base class for aggregated WebAuthn operations, combining data from multiple related events
/// grouped by transaction ID.
/// </summary>
public abstract class WebAuthnOperation
{
/// <summary>
/// The type of operation: Attestation (registration) or Assertion (authentication).
/// </summary>
public abstract string OperationType { get; }
/// <summary>
/// The transaction ID correlating all events in this operation.
/// </summary>
public Guid TransactionId { get; set; }
/// <summary>
/// When the operation started.
/// </summary>
public DateTime? TimeStarted { get; set; }
/// <summary>
/// Timestamp of the last event with this correlation/transaction ID.
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// The process ID that initiated the WebAuthN operation (from the service).
/// </summary>
public int ProcessId { get; set; }
/// <summary>
/// The relying party ID.
/// </summary>
public string? RpId { get; set; }
/// <summary>
/// SHA-256 hash of the relying party ID.
/// </summary>
public byte[]? RpIdHash { get; set; }
/// <summary>
/// Authenticator data flags (raw byte).
/// </summary>
public byte? Flags { get; set; }
/// <summary>
/// User Present (UP) flag - user presence test completed successfully.
/// </summary>
public bool? UserPresent => Flags.HasValue ? (Flags.Value & 0x01) != 0 : null;
/// <summary>
/// User Verified (UV) flag - user verification completed successfully.
/// </summary>
public bool? UserVerified => Flags.HasValue ? (Flags.Value & 0x04) != 0 : null;
/// <summary>
/// Attested Credential Data (AT) flag - authenticator data includes attested credential data.
/// </summary>
public bool? AttestationData => Flags.HasValue ? (Flags.Value & 0x40) != 0 : null;
/// <summary>
/// Extension Data (ED) flag - authenticator data includes extension data.
/// </summary>
public bool? ExtensionData => Flags.HasValue ? (Flags.Value & 0x80) != 0 : null;
/// <summary>
/// Signature counter.
/// </summary>
public uint? SignCount { get; set; }
/// <summary>
/// The credential ID.
/// </summary>
public byte[]? CredentialId { get; set; }
/// <summary>
/// Number of credentials involved in the request.
/// </summary>
public int? CredentialCount { get; set; }
/// <summary>
/// Authenticator Attestation GUID identifying the authenticator model.
/// </summary>
public Guid? AAGuid { get; set; }
/// <summary>
/// The name of the CTAP provider (e.g., MicrosoftPlatformProvider).
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// The device path of the authenticator.
/// </summary>
public string? DevicePath { get; set; }
/// <summary>
/// The manufacturer of the authenticator device.
/// </summary>
public string? Manufacturer { get; set; }
/// <summary>
/// The product name of the authenticator device.
/// </summary>
public string? Product { get; set; }
/// <summary>
/// Whether the authenticator uses the U2F protocol.
/// </summary>
public bool? U2fProtocol { get; set; }
/// <summary>
/// USB Vendor ID parsed from the device path (e.g., 0x1050 for Yubico).
/// </summary>
public int? VendorId { get; set; }
/// <summary>
/// USB Product ID parsed from the device path.
/// </summary>
public int? ProductId { get; set; }
/// <summary>
/// The user ID specified in the request (registration only).
/// </summary>
public byte[]? UserId { get; set; }
/// <summary>
/// The user name extracted from the CTAP2 CBOR request (registration only).
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// The user display name extracted from the CTAP2 CBOR request (registration only).
/// </summary>
public string? UserDisplayName { get; set; }
}
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace DSInternals.Win32.WebAuthn.Events;
/// <summary>
/// Aggregates raw WebAuthN events into <see cref="WebAuthnAttestationOperation"/> and <see cref="WebAuthnAssertionOperation"/> operations
/// grouped by transaction ID.
/// </summary>
public static class WebAuthnOperationBuilder
{
/// <summary>
/// Builds aggregated WebAuthn operations from a list of raw WebAuthN events.
/// </summary>
public static IReadOnlyList<WebAuthnOperation> Build(IEnumerable<WebAuthnEvent> events)
{
var operations = new List<WebAuthnOperation>();
// Group events by TransactionId (skip events without one)
var groups = events
.Where(e => e.TransactionId.HasValue)
.GroupBy(e => e.TransactionId!.Value);
foreach (var group in groups)
{
var eventList = group.ToList();
var transactionId = group.Key;
// Determine if this is an attestation (MakeCredential) or assertion (GetAssertion) transaction
bool hasAttestation = eventList.Any(e =>
e.EventId == WebAuthnEventId.MakeCredentialStarted ||
e.EventId == WebAuthnEventId.CborMakeCredentialRequest);
bool hasAssertion = eventList.Any(e =>
e.EventId == WebAuthnEventId.GetAssertionStarted ||
e.EventId == WebAuthnEventId.CborGetAssertionRequest);
if (hasAttestation)
{
// Only include successful attestations
bool hasAttestationError = eventList.Any(e => e.EventId == WebAuthnEventId.MakeCredentialError);
bool hasAttestationSuccess = eventList.Any(e => e.EventId == WebAuthnEventId.MakeCredentialCompleted);
if (hasAttestationSuccess && !hasAttestationError)
{
operations.Add(BuildAttestation(transactionId, eventList));
}
}
if (hasAssertion)
{
// Only include successful assertions
bool hasAssertionError = eventList.Any(e => e.EventId == WebAuthnEventId.GetAssertionError);
bool hasAssertionSuccess = eventList.Any(e => e.EventId == WebAuthnEventId.GetAssertionCompleted);
if (hasAssertionSuccess && !hasAssertionError)
{
operations.Add(BuildAssertion(transactionId, eventList));
}
}
}
return operations.OrderByDescending(o => o.TimeStarted ?? DateTime.MinValue).ToList();
}
private static WebAuthnAttestationOperation BuildAttestation(Guid transactionId, List<WebAuthnEvent> events)
{
var started = events.FirstOrDefault(e => e.EventId == WebAuthnEventId.MakeCredentialStarted);
var completed = events.FirstOrDefault(e => e.EventId == WebAuthnEventId.MakeCredentialCompleted);
var request = events.OfType<MakeCredentialRequestEvent>().FirstOrDefault();
var response = events.OfType<MakeCredentialResponseEvent>().FirstOrDefault();
var deviceInfo = events.OfType<DeviceInfoEvent>().FirstOrDefault();
var usbDevice = events.OfType<UsbDeviceEvent>().FirstOrDefault(e => e.AAGuid.HasValue);
// Get device info from either DeviceInfoEvent or UsbDeviceEvent
string? providerName = deviceInfo?.ProviderName;
string? devicePath = deviceInfo?.DevicePath ?? usbDevice?.DevicePath;
string? manufacturer = deviceInfo?.Manufacturer ?? usbDevice?.Manufacturer;
string? product = deviceInfo?.Product ?? usbDevice?.Product;
Guid? aaGuid = response?.AAGuid ?? deviceInfo?.AAGuid ?? usbDevice?.AAGuid;
bool? u2fProtocol = deviceInfo?.U2fProtocol ?? usbDevice?.U2fProtocol;
ParseVidPid(devicePath, out int? vendorId, out int? productId);
CtapMakeCredentialRequestParser.ParseUserInfo(request?.Request, out string? userName, out string? userDisplayName);
DateTime? endTime = events.Where(e => e.TimeCreated.HasValue).Max(e => e.TimeCreated);
return new WebAuthnAttestationOperation
{
TransactionId = transactionId,
TimeStarted = started?.TimeCreated,
EndTime = endTime,
ProcessId = started?.ProcessId ?? events.First().ProcessId,
RpId = request?.RpId,
RpIdHash = response?.RpIdHash,
Flags = response?.AuthenticatorFlags,
SignCount = response?.SignCount,
CredentialId = response?.CredentialId,
CredentialCount = request?.CredentialCount,
AAGuid = aaGuid,
ProviderName = providerName,
DevicePath = devicePath,
Manufacturer = manufacturer,
Product = product,
U2fProtocol = u2fProtocol,
VendorId = vendorId,
ProductId = productId,
UserId = request?.UserId,
UserName = userName,
UserDisplayName = userDisplayName,
RequireResidentKey = request?.RequireResidentKey,
CredentialParameterCount = request?.CredentialParameterCount,
AttestationFormatType = response?.AttestationFormatType,
ClientDataHashAlgId = request?.ClientDataHashAlgId,
U2fPublicKey = response?.U2fPublicKey,
PublicKey = response?.PublicKey
};
}
private static WebAuthnAssertionOperation BuildAssertion(Guid transactionId, List<WebAuthnEvent> events)
{
var started = events.FirstOrDefault(e => e.EventId == WebAuthnEventId.GetAssertionStarted);
var completed = events.FirstOrDefault(e => e.EventId == WebAuthnEventId.GetAssertionCompleted);
var request = events.OfType<GetAssertionRequestEvent>().FirstOrDefault();
var response = events.OfType<GetAssertionResponseEvent>().FirstOrDefault();
var deviceInfo = events.OfType<DeviceInfoEvent>().FirstOrDefault();
var usbDevice = events.OfType<UsbDeviceEvent>().FirstOrDefault(e => e.AAGuid.HasValue);
// Get device info from either DeviceInfoEvent or UsbDeviceEvent
string? providerName = deviceInfo?.ProviderName;
string? devicePath = deviceInfo?.DevicePath ?? usbDevice?.DevicePath;
string? manufacturer = deviceInfo?.Manufacturer ?? usbDevice?.Manufacturer;
string? product = deviceInfo?.Product ?? usbDevice?.Product;
Guid? aaGuid = deviceInfo?.AAGuid ?? usbDevice?.AAGuid;
bool? u2fProtocol = deviceInfo?.U2fProtocol ?? usbDevice?.U2fProtocol;
ParseVidPid(devicePath, out int? vendorId, out int? productId);
DateTime? endTime = events.Where(e => e.TimeCreated.HasValue).Max(e => e.TimeCreated);
return new WebAuthnAssertionOperation
{
TransactionId = transactionId,
TimeStarted = started?.TimeCreated,
EndTime = endTime,
ProcessId = started?.ProcessId ?? events.First().ProcessId,
RpId = request?.RpId,
RpIdHash = response?.RpIdHash,
Flags = response?.AuthenticatorFlags,
SignCount = response?.SignCount,
CredentialId = response?.CredentialId,
CredentialCount = request?.CredentialCount,
AAGuid = aaGuid,
ProviderName = providerName,
DevicePath = devicePath,
Manufacturer = manufacturer,
Product = product,
U2fProtocol = u2fProtocol,
VendorId = vendorId,
ProductId = productId,
ClientDataHashAlgId = request?.ClientDataHashAlgId
};
}
private static readonly Regex VidRegex = new(@"vid_([0-9a-fA-F]{4})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex PidRegex = new(@"pid_([0-9a-fA-F]{4})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static void ParseVidPid(string? devicePath, out int? vendorId, out int? productId)
{
vendorId = null;
productId = null;
if (string.IsNullOrEmpty(devicePath))
return;
var vidMatch = VidRegex.Match(devicePath);
if (vidMatch.Success && int.TryParse(vidMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int vid))
{
vendorId = vid;
}
var pidMatch = PidRegex.Match(devicePath);
if (pidMatch.Success && int.TryParse(pidMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int pid))
{
productId = pid;
}
}
}
@@ -0,0 +1,83 @@
{
"version": 1,
"dependencies": {
".NETFramework,Version=v4.8": {
"System.Formats.Cbor": {
"type": "Direct",
"requested": "[9.0.3, )",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg==",
"dependencies": {
"Microsoft.Bcl.HashCode": "1.1.1",
"System.Buffers": "4.5.1",
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.ValueTuple": "4.5.0"
}
},
"Microsoft.Bcl.HashCode": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA=="
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.1",
"contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"dependencies": {
"System.Buffers": "4.5.1",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
},
"System.Numerics.Vectors": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ=="
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.ValueTuple": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ=="
}
},
"net10.0-windows7.0": {
"System.Diagnostics.EventLog": {
"type": "Direct",
"requested": "[9.0.3, )",
"resolved": "9.0.3",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw=="
},
"System.Formats.Cbor": {
"type": "Direct",
"requested": "[9.0.3, )",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg=="
}
},
"net8.0-windows7.0": {
"System.Diagnostics.EventLog": {
"type": "Direct",
"requested": "[9.0.3, )",
"resolved": "9.0.3",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw=="
},
"System.Formats.Cbor": {
"type": "Direct",
"requested": "[9.0.3, )",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg=="
}
}
}
}
+3
View File
@@ -33,6 +33,9 @@
<!-- Turn off reference assembly generation -->
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<!-- Only use source-generated JSON serializers, not reflection-based -->
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
</PropertyGroup>
<!-- Note: Fido2.Models is currently not strong-name signed, so the Adapter DLLs cannot be signed. -->
+1
View File
@@ -21,6 +21,7 @@ namespace DSInternals.Win32.WebAuthn.PasskeyUI
containerRegistry.Register<ICredentialManagementViewModel, CredentialManagementViewModel>();
containerRegistry.Register<IAlgorithmSelectorViewModel, AlgorithmSelectorViewModel>();
containerRegistry.Register<IAuthenticatorListViewModel, AuthenticatorListViewModel>();
containerRegistry.Register<IEventLogViewModel, EventLogViewModel>();
// Dialogs
containerRegistry.RegisterDialog<NotificationDialog>();
+1
View File
@@ -35,6 +35,7 @@
<ItemGroup>
<ProjectReference Include="..\DSInternals.Win32.WebAuthn\DSInternals.Win32.WebAuthn.csproj" />
<ProjectReference Include="..\DSInternals.Win32.WebAuthn.Events\DSInternals.Win32.WebAuthn.Events.csproj" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,80 @@
<UserControl x:Class="DSInternals.Win32.WebAuthn.PasskeyUI.EventLogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DSInternals.Win32.WebAuthn.PasskeyUI"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"
d:DesignHeight="600" d:DesignWidth="1400">
<UserControl.Resources>
<local:ByteArrayToBase64UrlConverter x:Key="ByteArrayToBase64UrlConverter" />
</UserControl.Resources>
<DockPanel>
<Button DockPanel.Dock="Top" Command="{Binding LoadEventsCommand}" HorizontalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE72C;" FontFamily="{StaticResource FluentIcons}" FontSize="16" VerticalAlignment="Center" Margin="0,0,8,0" />
<AccessText Text="Load _Events" VerticalAlignment="Center" />
</StackPanel>
</Button>
<GroupBox Header="Passkey Operations">
<DataGrid x:Name="OperationsDataGrid"
ItemsSource="{Binding Operations}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="False"
CanUserDeleteRows="False"
HeadersVisibility="Column"
GridLinesVisibility="Horizontal"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
SelectionUnit="FullRow"
SelectionMode="Single"
Focusable="False"
IsTabStop="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Time Started" Binding="{Binding TimeStarted, StringFormat='{}{0:yyyy-MM-dd HH:mm:ss}'}" Width="160" MinWidth="80" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridTextColumn Header="Type" Binding="{Binding OperationType}" Width="110" MinWidth="80" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridTemplateColumn Header="Relying Party" Width="200" MinWidth="80" SortMemberPath="RpId">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:ClipboardEnabledTextBlock Text="{Binding RpId}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="User Name" Width="200" MinWidth="80" SortMemberPath="UserName">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:ClipboardEnabledTextBlock Text="{Binding UserName}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="AAGUID" Width="280" MinWidth="80" SortMemberPath="AAGuid">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:ClipboardEnabledTextBlock Text="{Binding AAGuid}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Provider" Binding="{Binding ProviderName}" Width="200" MinWidth="80" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridTextColumn Header="Product" Binding="{Binding Product}" Width="200" MinWidth="80" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridTemplateColumn Header="Credential ID" Width="280" MinWidth="80" SortMemberPath="CredentialId">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<local:ClipboardEnabledTextBlock Text="{Binding CredentialId, Converter={StaticResource ByteArrayToBase64UrlConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Credentials" Binding="{Binding CredentialCount}" Width="95" MinWidth="60" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridCheckBoxColumn Header="UP" Binding="{Binding UserPresent, Mode=OneWay}" Width="50" MinWidth="40" />
<DataGridCheckBoxColumn Header="UV" Binding="{Binding UserVerified, Mode=OneWay}" Width="50" MinWidth="40" />
<DataGridCheckBoxColumn Header="AT" Binding="{Binding AttestationData, Mode=OneWay}" Width="50" MinWidth="40" />
<DataGridCheckBoxColumn Header="ED" Binding="{Binding ExtensionData, Mode=OneWay}" Width="50" MinWidth="40" />
<DataGridTextColumn Header="Counter" Binding="{Binding SignCount}" Width="90" MinWidth="60" ElementStyle="{StaticResource DataGridTextElementStyle}" />
<DataGridCheckBoxColumn Header="U2F" Binding="{Binding U2fProtocol, Mode=OneWay}" Width="55" MinWidth="45" />
</DataGrid.Columns>
</DataGrid>
</GroupBox>
</DockPanel>
</UserControl>
@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace DSInternals.Win32.WebAuthn.PasskeyUI
{
public partial class EventLogView : UserControl
{
public EventLogView()
{
InitializeComponent();
}
}
}
@@ -0,0 +1,45 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using DSInternals.Win32.WebAuthn.Events;
using Prism.Commands;
using Prism.Dialogs;
using Prism.Mvvm;
namespace DSInternals.Win32.WebAuthn.PasskeyUI;
public class EventLogViewModel : BindableBase, IEventLogViewModel
{
private readonly IDialogService _dialogService;
public EventLogViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
LoadEventsCommand = new DelegateCommand(OnLoadEvents);
}
public ObservableCollection<WebAuthnOperation> Operations { get; } = [];
public ICommand LoadEventsCommand { get; }
private void OnLoadEvents()
{
try
{
Operations.Clear();
var events = WebAuthnEventReader.ReadEvents();
var operations = WebAuthnOperationBuilder.Build(events);
foreach (var operation in operations)
{
Operations.Add(operation);
}
}
catch (Exception ex)
{
DialogParameters parameters = new($"Message={ex.Message}");
_dialogService.ShowDialog(nameof(NotificationDialog), parameters);
}
}
}
@@ -0,0 +1,13 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using DSInternals.Win32.WebAuthn.Events;
namespace DSInternals.Win32.WebAuthn.PasskeyUI
{
public interface IEventLogViewModel
{
ObservableCollection<WebAuthnOperation> Operations { get; }
ICommand LoadEventsCommand { get; }
}
}
@@ -146,6 +146,9 @@
<TabItem Header="Authenticators">
<local:AuthenticatorListView />
</TabItem>
<TabItem Header="Event Log">
<local:EventLogView />
</TabItem>
</TabControl>
</DockPanel>
</Window>
+61
View File
@@ -42,6 +42,11 @@
"System.Threading.Tasks.Extensions": "4.6.3"
}
},
"Microsoft.Bcl.HashCode": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA=="
},
"Microsoft.Bcl.TimeProvider": {
"type": "Transitive",
"resolved": "8.0.1",
@@ -185,6 +190,18 @@
"System.ValueTuple": "4.5.0"
}
},
"System.Formats.Cbor": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg==",
"dependencies": {
"Microsoft.Bcl.HashCode": "1.1.1",
"System.Buffers": "4.5.1",
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.ValueTuple": "4.5.0"
}
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "8.15.0",
@@ -287,6 +304,12 @@
"System.IdentityModel.Tokens.Jwt": "[8.15.0, )",
"System.Text.Json": "[10.0.1, )"
}
},
"dsinternals.win32.webauthn.events": {
"type": "Project",
"dependencies": {
"System.Formats.Cbor": "[9.0.3, )"
}
}
},
".NETFramework,Version=v4.8/win-arm64": {},
@@ -431,6 +454,11 @@
"resolved": "9.0.537",
"contentHash": "Pzp5MGUuhAyKXZUbHVYNWLGF/eA3sScqDN6VrzbWlKj85R0IS0q+JXe99umynso2xhXAe+1jrQCCkgqmEFCBng=="
},
"System.Formats.Cbor": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "8.15.0",
@@ -460,6 +488,12 @@
"PeterO.Cbor": "[4.5.5, )",
"System.IdentityModel.Tokens.Jwt": "[8.15.0, )"
}
},
"dsinternals.win32.webauthn.events": {
"type": "Project",
"dependencies": {
"System.Formats.Cbor": "[9.0.3, )"
}
}
},
"net10.0-windows7.0/win-arm64": {},
@@ -604,6 +638,16 @@
"resolved": "9.0.537",
"contentHash": "Pzp5MGUuhAyKXZUbHVYNWLGF/eA3sScqDN6VrzbWlKj85R0IS0q+JXe99umynso2xhXAe+1jrQCCkgqmEFCBng=="
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw=="
},
"System.Formats.Cbor": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "tRqH5Ze3/2anAQ5TWd3NB50l74afOFX7YChfpVFKPJl5KkrK+sg5J4xtuDT0glNUt7LkexrSfYzGcgalaqmVsg=="
},
"System.IdentityModel.Tokens.Jwt": {
"type": "Transitive",
"resolved": "8.15.0",
@@ -659,9 +703,21 @@
"System.Security.Cryptography.Cng": "[5.0.0, )",
"System.Text.Json": "[10.0.1, )"
}
},
"dsinternals.win32.webauthn.events": {
"type": "Project",
"dependencies": {
"System.Diagnostics.EventLog": "[9.0.3, )",
"System.Formats.Cbor": "[9.0.3, )"
}
}
},
"net8.0-windows7.0/win-arm64": {
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw=="
},
"System.Security.Cryptography.Cng": {
"type": "Transitive",
"resolved": "5.0.0",
@@ -674,6 +730,11 @@
}
},
"net8.0-windows7.0/win-x64": {
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "9.0.3",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw=="
},
"System.Security.Cryptography.Cng": {
"type": "Transitive",
"resolved": "5.0.0",
+1
View File
@@ -29,6 +29,7 @@
<Project Path="Src/DSInternals.Win32.WebAuthn/DSInternals.Win32.WebAuthn.csproj" />
<Project Path="Src/DSInternals.Win32.WebAuthn.Tests/DSInternals.Win32.WebAuthn.Tests.csproj" />
<Project Path="Src/PasskeyUI/PasskeyUI.csproj" DefaultStartup="true" />
<Project Path="Src/DSInternals.Win32.WebAuthn.Events/DSInternals.Win32.WebAuthn.Events.csproj" />
<Project Path="Src/DSInternals.Win32.WebAuthn.Adapter/DSInternals.Win32.WebAuthn.Adapter.csproj" />
<Project Path="Src/DSInternals.Win32.WebAuthn.Adapter.Tests/DSInternals.Win32.WebAuthn.Adapter.Tests.csproj" />
</Solution>