diff --git a/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs b/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs
index 0a6015a..addf433 100644
--- a/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs
+++ b/Module/Cmdlets/FIDO2/NewFIDO2Credential.cs
@@ -1,19 +1,30 @@
///
-/// Creates a new FIDO2 credential on a YubiKey.
-/// Supports creating credentials with various parameters including relying party information,
-/// user data, and authentication options. Requires a YubiKey with FIDO2 support and
-/// administrator privileges on Windows.
+/// Creates a new FIDO2 discoverable credential on a YubiKey.
+/// Supports creating credentials with various parameters including Relying Party (RP)
+/// information, user data, and authentication options. Requires a YubiKey with FIDO2 support
+/// and administrator privileges on Windows. When used with only -RelyingPartyID and -Username
+/// (Synthetic parameter set), the cmdlet auto-generates a cryptographic challenge and
+/// random user ID so no external IdP is needed.
///
/// .EXAMPLE
-/// $challenge = New-YubiKeyFIDO2Challenge
-/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.com" -Username "user@example.com" -Challenge $challenge
-/// Creates a new FIDO2 credential for example.com with the specified username
+/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.local" -Username "alice@example.local"
+/// Creates a synthetic credential (without an actual IdP) with a default display name.
+///
+/// .EXAMPLE
+/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.local" -Username "alice@example.local" -UserDisplayName "Alice Smith"
+/// Creates a synthetic credential (without an actual IdP) with a custom display name.
+///
+/// .EXAMPLE
+/// $challengeB64Url = "<challenge from relying party registerBegin response>"
+/// $challenge = [powershellYK.FIDO2.Challenge]::FromBase64URLEncoded($challengeB64Url)
+/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.com" -RelyingPartyName "Example" -Username "user@example.com" -UserID ([byte[]](0x01)) -Challenge $challenge
+/// Creates a credential using the challenge issued by the relying party during registration.
///
/// .EXAMPLE
/// $rp = Get-YubiKeyFIDO2Credential | Select-Object -First 1 -ExpandProperty RelyingParty
-/// $challenge = New-YubiKeyFIDO2Challenge
-/// New-YubiKeyFIDO2Credential -RelyingParty $rp -Username "user@example.com" -Challenge $challenge
-/// Creates a new FIDO2 credential using an existing relying party object
+/// $challenge = [powershellYK.FIDO2.Challenge]::CreateSyntheticChallenge($rp.Id)
+/// New-YubiKeyFIDO2Credential -RelyingParty $rp -Username "user@example.com" -UserID ([byte[]](0x01)) -Challenge $challenge
+/// Creates a credential reusing a relying party from an existing credential with a locally generated challenge.
///
// Imports
@@ -23,6 +34,7 @@
using powershellYK.support;
using Yubico.YubiKey.Cryptography;
using powershellYK.FIDO2;
+using powershellYK.support.FIDO2;
namespace powershellYK.Cmdlets.Fido
{
@@ -32,10 +44,12 @@ public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet
// Parameters for relying party information
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Specify which relayingParty (site) this credential is regards to.", ParameterSetName = "UserData-HostData")]
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Specify which relayingParty (site) this credential is regards to.", ParameterSetName = "UserEntity-HostData")]
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Relying party ID (domain) for the credential.", ParameterSetName = "Synthetic")]
public required string RelyingPartyID { private get; set; }
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendlyname for the relayingParty.", ParameterSetName = "UserData-HostData")]
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendlyname for the relayingParty.", ParameterSetName = "UserEntity-HostData")]
+ [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendly name for the relying party. Defaults to RelyingPartyID.", ParameterSetName = "Synthetic")]
public required string RelyingPartyName { private get; set; }
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "RelaingParty object.", ParameterSetName = "UserData-RelyingParty")]
@@ -45,10 +59,12 @@ public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet
// Parameters for user information
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username to create credental for.", ParameterSetName = "UserData-RelyingParty")]
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username to create credental for.", ParameterSetName = "UserData-HostData")]
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username for the credential.", ParameterSetName = "Synthetic")]
public required string Username { private get; set; }
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserDisplayName to create credental for.", ParameterSetName = "UserData-RelyingParty")]
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserDisplayName to create credental for.", ParameterSetName = "UserData-HostData")]
+ [Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Display name for the user. Defaults to Username.", ParameterSetName = "Synthetic")]
public string? UserDisplayName { private get; set; }
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "UserID.", ParameterSetName = "UserData-RelyingParty")]
@@ -56,8 +72,11 @@ public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet
public byte[]? UserID { private get; set; }
// Parameters for credential configuration
- [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challange.")]
- public required Challenge Challenge { private get; set; }
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserData-HostData")]
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserData-RelyingParty")]
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserEntity-HostData")]
+ [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserEntity-RelyingParty")]
+ public Challenge? Challenge { private get; set; }
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Should this credential be discoverable.")]
public bool Discoverable { private get; set; } = true;
@@ -103,6 +122,20 @@ protected override void ProcessRecord()
// Set up key collector for PIN operations
fido2Session.KeyCollector = YubiKeyModule._KeyCollector.YKKeyCollectorDelegate;
+ if (ParameterSetName == "Synthetic")
+ {
+ WriteDebug("Synthetic mode: generating Challenge and UserID automatically.");
+ Challenge = FIDO2.Challenge.CreateSyntheticChallenge(RelyingPartyID);
+ RelyingParty = new RelyingParty(RelyingPartyID) { Name = RelyingPartyName ?? RelyingPartyID };
+ byte[] syntheticUserId = SyntheticCredentialHelper.GenerateUserID();
+ WriteDebug($"Generated synthetic UserID: {Converter.ByteArrayToString(syntheticUserId)}");
+ UserEntity = new UserEntity(syntheticUserId.AsMemory())
+ {
+ Name = Username,
+ DisplayName = UserDisplayName ?? Username,
+ };
+ }
+
// Configure relying party information
if (RelyingParty is null)
{
@@ -142,7 +175,7 @@ protected override void ProcessRecord()
{
type = "webauthn.create",
origin = $"https://{RelyingParty.Id}",
- challenge = Challenge.Base64URLEncode(),
+ challenge = Challenge!.Base64URLEncode(),
};
var clientDataJSON = System.Text.Json.JsonSerializer.Serialize(clientData);
@@ -160,10 +193,12 @@ protected override void ProcessRecord()
}
// Create and return the credential
- WriteDebug($"Sending new credential data into SDK");
+ WriteDebug($"Promting for touch to complete the credential creation...");
+ Console.WriteLine("Touch the YubiKey...");
MakeCredentialData returnvalue = fido2Session.MakeCredential(make);
var credData = new CredentialData(returnvalue, clientDataJSON, UserEntity!, RelyingParty);
+ WriteInformation($"Credential created for {UserEntity!.DisplayName ?? UserEntity.Name} using RP: {RelyingParty.Id}.", new string[] { "FIDO2", "Info" });
WriteObject(credData);
}
}
diff --git a/Module/support/FIDO2/SyntheticCredentialHelper.cs b/Module/support/FIDO2/SyntheticCredentialHelper.cs
new file mode 100644
index 0000000..8cbacb9
--- /dev/null
+++ b/Module/support/FIDO2/SyntheticCredentialHelper.cs
@@ -0,0 +1,24 @@
+///
+/// Helpers for creating FIDO2 synthetic credentials without an IdP.
+/// GenerateUserID returns 32 cryptographically random bytes suitable for
+/// WebAuthn user.id (spec allows 1-64 bytes; value must not contain PII).
+///
+
+// Imports
+using Yubico.YubiKey.Cryptography;
+
+namespace powershellYK.support.FIDO2
+{
+ // Helpers for creating FIDO2 synthetic credentials without an IdP
+ public static class SyntheticCredentialHelper
+ {
+ // Generates a 32-byte cryptographically random user ID
+ public static byte[] GenerateUserID()
+ {
+ byte[] userId = new byte[32];
+ var rng = CryptographyProviders.RngCreator();
+ rng.GetBytes(userId);
+ return userId;
+ }
+ }
+}
diff --git a/Module/types/FIDO2/Challenge.cs b/Module/types/FIDO2/Challenge.cs
index dab0ff2..bda90ff 100644
--- a/Module/types/FIDO2/Challenge.cs
+++ b/Module/types/FIDO2/Challenge.cs
@@ -3,13 +3,13 @@
/// Handles challenge generation, encoding, and conversion between formats.
///
/// .EXAMPLE
-/// # Create a challenge from a base64 string
+/// # Create a challenge from a base64 string (from an IdP registerBegin response)
/// $challenge = [powershellYK.FIDO2.Challenge]::new("SGVsbG8gV29ybGQ=")
/// Write-Host $challenge.ToString()
///
/// .EXAMPLE
-/// # Create a fake challenge for testing
-/// $challenge = [powershellYK.FIDO2.Challenge]::FakeChallange("example.com")
+/// # Create a synthetic challenge locally (without an actual IdP)
+/// $challenge = [powershellYK.FIDO2.Challenge]::CreateSyntheticChallenge("example.local")
/// Write-Host $challenge.Base64URLEncode()
///
@@ -37,10 +37,16 @@ public Challenge(byte[] value)
this._challenge = value;
}
- // Generates a fake challenge for testing purposes
+ public static Challenge CreateSyntheticChallenge(string relyingPartyID)
+ {
+ return new Challenge(BuildSyntheticChallengeBytes(32));
+ }
+
+ // Kept for backward compatibility; Pester tests and earlier scripts reference this name.
+ [System.Obsolete("Use CreateSyntheticChallenge instead.")]
public static Challenge FakeChallange(string relyingPartyID)
{
- return new Challenge(BuildFakeClientDataHash(relyingPartyID));
+ return CreateSyntheticChallenge(relyingPartyID);
}
// Converts the challenge to a string representation
@@ -111,23 +117,12 @@ private static string AddMissingPadding(string base64)
return base64;
}
- // Builds a fake client data hash for testing
- private static byte[] BuildFakeClientDataHash(string relyingPartyId)
+ private static byte[] BuildSyntheticChallengeBytes(int length = 32)
{
- // Convert relying party ID to bytes
- byte[] idBytes = System.Text.Encoding.Unicode.GetBytes(relyingPartyId);
-
- // Generate random challenge bytes
- var randomObject = CryptographyProviders.RngCreator();
- byte[] randomBytes = new byte[16];
- randomObject.GetBytes(randomBytes);
-
- // Create hash of random bytes and relying party ID
- var digester = CryptographyProviders.Sha256Creator();
- _ = digester.TransformBlock(randomBytes, 0, randomBytes.Length, null, 0);
- _ = digester.TransformFinalBlock(idBytes, 0, idBytes.Length);
-
- return digester.Hash!;
+ byte[] randomBytes = new byte[length];
+ var rng = CryptographyProviders.RngCreator();
+ rng.GetBytes(randomBytes);
+ return randomBytes;
}
#endregion // Support Methods
diff --git a/Module/types/FIDO2/CredentialData.cs b/Module/types/FIDO2/CredentialData.cs
index 647d841..94b1a67 100644
--- a/Module/types/FIDO2/CredentialData.cs
+++ b/Module/types/FIDO2/CredentialData.cs
@@ -19,6 +19,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using Yubico.YubiKey.Fido2;
+using Yubico.YubiKey.Fido2.Cose;
namespace powershellYK.FIDO2
{
@@ -28,6 +29,8 @@ public class CredentialData
// Properties for accessing credential data
public MakeCredentialData MakeCredentialData { get { return this._makeCredentialData; } }
public string ClientDataJSON { get { return this._clientDataJSON; } }
+ public CoseKey? PublicKey => _makeCredentialData.AuthenticatorData.CredentialPublicKey;
+ public ReadOnlyMemory? CredentialId => _makeCredentialData.AuthenticatorData.CredentialId?.Id;
// Internal storage for credential components
private readonly string _clientDataJSON;
diff --git a/Pester/310-FIDO2.tests.ps1 b/Pester/310-FIDO2.tests.ps1
index 39e9558..848e4ff 100644
--- a/Pester/310-FIDO2.tests.ps1
+++ b/Pester/310-FIDO2.tests.ps1
@@ -10,6 +10,15 @@ Describe "FIDO2 Tests" -Tag @("FIDO2") {
{Connect-YubikeyFIDO2 -PIN (ConvertTo-SecureString -String "654321" -AsPlainText -Force)} | Should -Not -Throw
{Set-YubikeyFIDO2PIN -OldPIN (ConvertTo-SecureString -String "654321" -AsPlainText -Force) -NewPIN (ConvertTo-SecureString -String "123456" -AsPlainText -Force)} | Should -Not -Throw
}
+ It -Name "Create synthetic credential (no IdP)" -Test {
+ {New-YubiKeyFIDO2Credential -RelyingPartyID 'powershellYK-synthetic' -Username 'syntheticUser'} | Should -Not -Throw
+ (Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic' }).UserName | Should -Be 'syntheticUser'
+ {Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic' } | ForEach-Object { Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false }} | Should -Not -Throw
+ }
+ It -Name "Create synthetic credential with display name" -Test {
+ {New-YubiKeyFIDO2Credential -RelyingPartyID 'powershellYK-synthetic2' -Username 'synUser2' -UserDisplayName 'Synthetic User Two'} | Should -Not -Throw
+ {Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic2' } | ForEach-Object { Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false }} | Should -Not -Throw
+ }
It -Name "Clear all credentials" -Test {
{Get-YubiKeyFIDO2Credential|%{Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false}} | Should -Not -Throw
(Get-YubiKeyFIDO2Credential).Count | Should -Be 0