Skip to content

Commit

Permalink
Prepare WebAuthn assertion
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Aug 4, 2024
1 parent cbc282f commit 594bd72
Show file tree
Hide file tree
Showing 6 changed files with 495 additions and 82 deletions.
12 changes: 12 additions & 0 deletions Guard.Core/Models/AuthFileData.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
namespace Guard.Core.Models
{
public class WebauthnDevice
{
public required string Id { get; set; }
public string? EncryptedName { get; set; }
public required string PublicKey { get; set; }
}

public class AuthFileData
{
public required int Version { get; set; }
Expand All @@ -24,5 +31,10 @@ public class AuthFileData
/// Additionally, this makes it slightly harder to access the sensitive data.
/// </summary>
public string? InsecureMainKey { get; set; }

/// <summary>
/// Stores a list of WebAuthn devices (external FIDO2 security keys, e.g. YubiKey).
/// </summary>
public List<WebauthnDevice>? WebAuthn { get; set; }
}
}
17 changes: 17 additions & 0 deletions Guard.Core/Security/Auth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,5 +390,22 @@ public static string GetInstallationID()
ArgumentNullException.ThrowIfNull(authData);
return authData.InstallationID;
}

internal static List<WebauthnDevice>? GetWebAuthnDevices()
{
ArgumentNullException.ThrowIfNull(authData);
return authData.WebAuthn;
}

internal static async void AddWebAuthnDevice(WebauthnDevice device)
{
ArgumentNullException.ThrowIfNull(authData);
if (authData.WebAuthn == null)
{
authData.WebAuthn = new();
}
authData.WebAuthn.Add(device);
await SaveFile();
}
}
}
169 changes: 88 additions & 81 deletions Guard.Core/Security/WebAuthn/WebAuthnHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Text.Json;
using Guard.Core.Security.WebAuthn.entities;
using NeoSmart.Utils;
using OtpNet;

namespace Guard.Core.Security.WebAuthn
{
Expand All @@ -18,7 +17,7 @@ public static int GetApiVersion()
return WebAuthnInterop.GetApiVersion();
}

public static void Register(IntPtr windowHandle)
public static async Task<(bool success, string? error)> Register(IntPtr windowHandle)
{
if (!IsSupported())
{
Expand All @@ -27,97 +26,105 @@ public static void Register(IntPtr windowHandle)
);
}

RelayingPartyInfo relayingPartyInfo =
new() { Id = "win.2faguard.app", Name = "2FAGuard", };
var challenge = EncryptionHelper.GetRandomBytes(32);
var extensions = new List<WebAuthnCreationExtensionInput>
{
new HmacSecretCreationExtension()
};

UserInfo userInfo =
new()
{
UserId = Encoding.UTF8.GetBytes(Auth.GetInstallationID()),
Name = "2FAGuard",
DisplayName = "2FAGuard"
};
WindowsHello.FocusSecurityPrompt();

List<CoseCredentialParameter> coseCredentialParameters =
new()
{
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P521_WITH_SHA512
},
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P384_WITH_SHA384
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.EDDSA },
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P256_WITH_SHA256
},
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.RSASSA_PKCS1_V1_5_WITH_SHA512,
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.RSA_PSS_WITH_SHA512 },
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.RSASSA_PKCS1_V1_5_WITH_SHA384,
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.RSA_PSS_WITH_SHA384 },
new CoseCredentialParameter
return await Task.Run<(bool success, string? error)>(() =>
{
/*
* ToDo:
* - excludeCredentials
* - authenticatorSelection
* - PreferResidentKey
* - cancellation
* - credential protection
*/
var res = WebAuthnInterop.CreateCredential(
windowHandle,
WebAuthnSettings.RelayingPartyInfo,
WebAuthnSettings.UserInfo,
WebAuthnSettings.CoseCredentialParameters,
WebAuthnSettings.GetClientData(
challenge,
WebAuthnSettings.ClientDataType.Create
),
new AuthenticatorMakeCredentialOptions
{
Algorithm = CoseAlgorithm.RSASSA_PKCS1_V1_5_WITH_SHA256,
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
Extensions = extensions,
UserVerificationRequirement = UserVerificationRequirement.Preferred
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.RSA_PSS_WITH_SHA256 },
};
out var credential
);

if (res != WebAuthnHResult.Ok)
{
return (false, res.ToString());
}

// Todo check challenge


Log.Logger.Information(
"WebAuthn credential registered successfully: {credential}",
credential
);

return (true, null);
});
}

public static async Task<(bool success, string? error)> Assert(IntPtr windowHandle)
{
if (!IsSupported())
{
throw new PlatformNotSupportedException(
"WebAuthn API is not available on this platform."
);
}

var challenge = EncryptionHelper.GetRandomBytes(32);

var extensions = new List<WebAuthnCreationExtensionInput>();
var extensions = new List<WebAuthnAssertionExtensionInput>
{
new HmacSecretAssertionExtension()
};

WindowsHello.FocusSecurityPrompt();

/*
* ToDo:
* - excludeCredentials
* - authenticatorSelection
* - PreferResidentKey
* - cancellation
* - credential protection
* Todos:
* - AllowedCredentials
*/
var res = WebAuthnInterop.AuthenticatorMakeCredential(
windowHandle,
relayingPartyInfo,
userInfo,
coseCredentialParameters,
new ClientData()
{
ClientDataJSON = JsonSerializer.SerializeToUtf8Bytes(
new
{
type = "webauthn.create",
challenge = UrlBase64.Encode(challenge),
origin = "win.2faguard.app"
}
),
HashAlgorithm = HashAlgorithm.Sha512
},
new AuthenticatorMakeCredentialOptions
{
AuthenticatorAttachment = AuthenticatorAttachment.CrossPlatform,
Extensions = extensions,
UserVerificationRequirement = UserVerificationRequirement.Preferred
},
out var credential
);

if (res != WebAuthnHResult.Ok)
return await Task.Run<(bool success, string? error)>(() =>
{
throw new Exception($"Failed to register WebAuthn credential: {res}");
}
var res = WebAuthnInterop.GetAssertion(
windowHandle,
WebAuthnSettings.Origin,
WebAuthnSettings.GetClientData(challenge, WebAuthnSettings.ClientDataType.Get),
new AuthenticatorGetAssertionOptions
{
UserVerificationRequirement = UserVerificationRequirement.Preferred
},
out var assertion
);

if (res != WebAuthnHResult.Ok)
{
return (false, res.ToString());
}

// Todo check challenge

Log.Logger.Information("WebAuthn assertion successful: {assertion}", assertion);

Log.Logger.Information(
"WebAuthn credential registered successfully: {credential}",
credential
);
return (true, null);
});
}
}
}
49 changes: 48 additions & 1 deletion Guard.Core/Security/WebAuthn/WebAuthnInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private static extern void WebAuthNFreeCredentialAttestation(
IntPtr rawCredentialAttestation
);

public static WebAuthnHResult AuthenticatorMakeCredential(
public static WebAuthnHResult CreateCredential(
IntPtr window,
RelayingPartyInfo rp,
UserInfo user,
Expand Down Expand Up @@ -119,5 +119,52 @@ out var rawCredPtr

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr FindWindow(string lpClassName, IntPtr ZeroOnly);

[DllImport("webauthn.dll", CharSet = CharSet.Unicode)]
private static extern WebAuthnHResult WebAuthNAuthenticatorGetAssertion(
[In] IntPtr hWnd,
[In, Optional] string rpId,
[In] RawClientData rawClientData,
[In, Optional] RawAuthenticatorGetAssertionOptions rawGetAssertionOptions,
[Out] out IntPtr rawAssertionPtr
);

[DllImport("webauthn.dll", EntryPoint = "WebAuthNFreeAssertion")]
private static extern void FreeRawAssertion(IntPtr rawAssertion);

public static WebAuthnHResult GetAssertion(
IntPtr window,
string rpId,
ClientData clientData,
AuthenticatorGetAssertionOptions getOptions,
out Assertion? assertion
)
{
assertion = null;

var rawClientData = new RawClientData(clientData);
var rawGetOptions =
getOptions == null ? null : new RawAuthenticatorGetAssertionOptions(getOptions);

var res = WebAuthNAuthenticatorGetAssertion(
window,
rpId,
rawClientData,
rawGetOptions,

Check warning on line 153 in Guard.Core/Security/WebAuthn/WebAuthnInterop.cs

View workflow job for this annotation

GitHub Actions / Test

Possible null reference argument for parameter 'rawGetAssertionOptions' in 'WebAuthnHResult WebAuthnInterop.WebAuthNAuthenticatorGetAssertion(nint hWnd, string rpId, RawClientData rawClientData, RawAuthenticatorGetAssertionOptions rawGetAssertionOptions, out nint rawAssertionPtr)'.
out var rawAsnPtr
);

if (rawAsnPtr != IntPtr.Zero)
{
var rawAssertion = Marshal.PtrToStructure<RawAssertion>(rawAsnPtr);
assertion = rawAssertion?.MarshalToPublic();
FreeRawAssertion(rawAsnPtr);
}

rawClientData.Dispose();
rawGetOptions?.Dispose();

return res;
}
}
}
82 changes: 82 additions & 0 deletions Guard.Core/Security/WebAuthn/WebAuthnSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Text;
using System.Text.Json;
using Guard.Core.Security.WebAuthn.entities;
using NeoSmart.Utils;

namespace Guard.Core.Security.WebAuthn
{
internal class WebAuthnSettings
{
public static string Origin = "2faguard.app";

public static RelayingPartyInfo RelayingPartyInfo
{
get => new() { Id = Origin, Name = "2FAGuard", };
}

public static UserInfo UserInfo
{
get =>
new()
{
UserId = Encoding.UTF8.GetBytes(Auth.GetInstallationID()),
Name = "2FAGuard User",
DisplayName = "2FAGuard User"
};
}

public static List<CoseCredentialParameter> CoseCredentialParameters
{
get =>
new()
{
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P521_WITH_SHA512
},
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P384_WITH_SHA384
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.EDDSA },
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.ECDSA_P256_WITH_SHA256
},
new CoseCredentialParameter
{
Algorithm = CoseAlgorithm.RSASSA_PKCS1_V1_5_WITH_SHA512,
},
new CoseCredentialParameter { Algorithm = CoseAlgorithm.RSA_PSS_WITH_SHA512 },
};
}

public enum ClientDataType
{
Create,
Get
}

public static ClientData GetClientData(byte[] challenge, ClientDataType type)
{
string typeString = type switch
{
ClientDataType.Create => "webauthn.create",
ClientDataType.Get => "webauthn.get",
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
return new ClientData()
{
ClientDataJSON = JsonSerializer.SerializeToUtf8Bytes(
new
{
type = typeString,
challenge = UrlBase64.Encode(challenge),
origin = Origin
}
),
HashAlgorithm = HashAlgorithm.Sha512
};
}
}
}
Loading

0 comments on commit 594bd72

Please sign in to comment.