Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions Module/Cmdlets/FIDO2/NewFIDO2Credential.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
/// <summary>
/// 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 = "&lt;challenge from relying party registerBegin response&gt;"
/// $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.
/// </summary>

// Imports
Expand All @@ -23,6 +34,7 @@
using powershellYK.support;
using Yubico.YubiKey.Cryptography;
using powershellYK.FIDO2;
using powershellYK.support.FIDO2;

namespace powershellYK.Cmdlets.Fido
{
Expand All @@ -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")]
Expand All @@ -45,19 +59,24 @@ 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")]
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "UserID.", ParameterSetName = "UserData-HostData")]
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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
}
Expand Down
24 changes: 24 additions & 0 deletions Module/support/FIDO2/SyntheticCredentialHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/// <summary>
/// Helpers for creating FIDO2 synthetic credentials without an IdP.
/// <c>GenerateUserID</c> returns 32 cryptographically random bytes suitable for
/// WebAuthn <c>user.id</c> (spec allows 1-64 bytes; value must not contain PII).
/// </summary>

// 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;
}
}
}
37 changes: 16 additions & 21 deletions Module/types/FIDO2/Challenge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
/// </summary>

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Module/types/FIDO2/CredentialData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Runtime.CompilerServices;
using System.Text;
using Yubico.YubiKey.Fido2;
using Yubico.YubiKey.Fido2.Cose;

namespace powershellYK.FIDO2
{
Expand All @@ -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<byte>? CredentialId => _makeCredentialData.AuthenticatorData.CredentialId?.Id;

// Internal storage for credential components
private readonly string _clientDataJSON;
Expand Down
9 changes: 9 additions & 0 deletions Pester/310-FIDO2.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading