Skip to content

Commit 029dfb4

Browse files
authored
Add "synthetic"/ "mock" FIDO2 credential creation. (#200)
Add support for synthetic (mock) FIDO2 credential creation Add support for “synthetic” (locally generated) FIDO2 credential creation and rename FakeChallange to CreateSyntheticChallenge. Introduce a Synthetic parameter set on New-YubiKeyFIDO2Credential that: Generates challenge, user ID, and user entity locally Requires only -RelyingPartyID and -Username Eliminates the need for an external IdP Goal: Enable standalone FIDO2 credential creation and simplify workflows for large blob and PRT scenarios by allowing credentials to be created on demand. Details Challenge.cs: Add CreateSyntheticChallenge (32 random bytes); deprecate FakeChallange as an [Obsolete] wrapper for backward compatibility SyntheticCredentialHelper.cs (new): GenerateUserID returns 32 cryptographically secure random bytes for WebAuthn user.id NewFIDO2Credential.cs: Add Synthetic parameter set Default UserDisplayName to Username Populate synthetic values in ProcessRecord Add “Touch the YubiKey…” prompt and WriteInformation confirmation Update XML documentation and examples CredentialData.cs: Add PublicKey and CredentialId convenience properties 310-FIDO2.tests.ps1: Add Pester tests for the synthetic parameter set
1 parent 5e68343 commit 029dfb4

5 files changed

Lines changed: 101 additions & 35 deletions

File tree

Module/Cmdlets/FIDO2/NewFIDO2Credential.cs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
/// <summary>
2-
/// Creates a new FIDO2 credential on a YubiKey.
3-
/// Supports creating credentials with various parameters including relying party information,
4-
/// user data, and authentication options. Requires a YubiKey with FIDO2 support and
5-
/// administrator privileges on Windows.
2+
/// Creates a new FIDO2 discoverable credential on a YubiKey.
3+
/// Supports creating credentials with various parameters including Relying Party (RP)
4+
/// information, user data, and authentication options. Requires a YubiKey with FIDO2 support
5+
/// and administrator privileges on Windows. When used with only -RelyingPartyID and -Username
6+
/// (Synthetic parameter set), the cmdlet auto-generates a cryptographic challenge and
7+
/// random user ID so no external IdP is needed.
68
///
79
/// .EXAMPLE
8-
/// $challenge = New-YubiKeyFIDO2Challenge
9-
/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.com" -Username "user@example.com" -Challenge $challenge
10-
/// Creates a new FIDO2 credential for example.com with the specified username
10+
/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.local" -Username "alice@example.local"
11+
/// Creates a synthetic credential (without an actual IdP) with a default display name.
12+
///
13+
/// .EXAMPLE
14+
/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.local" -Username "alice@example.local" -UserDisplayName "Alice Smith"
15+
/// Creates a synthetic credential (without an actual IdP) with a custom display name.
16+
///
17+
/// .EXAMPLE
18+
/// $challengeB64Url = "&lt;challenge from relying party registerBegin response&gt;"
19+
/// $challenge = [powershellYK.FIDO2.Challenge]::FromBase64URLEncoded($challengeB64Url)
20+
/// New-YubiKeyFIDO2Credential -RelyingPartyID "example.com" -RelyingPartyName "Example" -Username "user@example.com" -UserID ([byte[]](0x01)) -Challenge $challenge
21+
/// Creates a credential using the challenge issued by the relying party during registration.
1122
///
1223
/// .EXAMPLE
1324
/// $rp = Get-YubiKeyFIDO2Credential | Select-Object -First 1 -ExpandProperty RelyingParty
14-
/// $challenge = New-YubiKeyFIDO2Challenge
15-
/// New-YubiKeyFIDO2Credential -RelyingParty $rp -Username "user@example.com" -Challenge $challenge
16-
/// Creates a new FIDO2 credential using an existing relying party object
25+
/// $challenge = [powershellYK.FIDO2.Challenge]::CreateSyntheticChallenge($rp.Id)
26+
/// New-YubiKeyFIDO2Credential -RelyingParty $rp -Username "user@example.com" -UserID ([byte[]](0x01)) -Challenge $challenge
27+
/// Creates a credential reusing a relying party from an existing credential with a locally generated challenge.
1728
/// </summary>
1829

1930
// Imports
@@ -23,6 +34,7 @@
2334
using powershellYK.support;
2435
using Yubico.YubiKey.Cryptography;
2536
using powershellYK.FIDO2;
37+
using powershellYK.support.FIDO2;
2638

2739
namespace powershellYK.Cmdlets.Fido
2840
{
@@ -32,10 +44,12 @@ public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet
3244
// Parameters for relying party information
3345
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Specify which relayingParty (site) this credential is regards to.", ParameterSetName = "UserData-HostData")]
3446
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Specify which relayingParty (site) this credential is regards to.", ParameterSetName = "UserEntity-HostData")]
47+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Relying party ID (domain) for the credential.", ParameterSetName = "Synthetic")]
3548
public required string RelyingPartyID { private get; set; }
3649

3750
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendlyname for the relayingParty.", ParameterSetName = "UserData-HostData")]
3851
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendlyname for the relayingParty.", ParameterSetName = "UserEntity-HostData")]
52+
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Friendly name for the relying party. Defaults to RelyingPartyID.", ParameterSetName = "Synthetic")]
3953
public required string RelyingPartyName { private get; set; }
4054

4155
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "RelaingParty object.", ParameterSetName = "UserData-RelyingParty")]
@@ -45,19 +59,24 @@ public class NewYubikeyFIDO2CredentialCmdlet : PSCmdlet
4559
// Parameters for user information
4660
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username to create credental for.", ParameterSetName = "UserData-RelyingParty")]
4761
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username to create credental for.", ParameterSetName = "UserData-HostData")]
62+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Username for the credential.", ParameterSetName = "Synthetic")]
4863
public required string Username { private get; set; }
4964

5065
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserDisplayName to create credental for.", ParameterSetName = "UserData-RelyingParty")]
5166
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "UserDisplayName to create credental for.", ParameterSetName = "UserData-HostData")]
67+
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Display name for the user. Defaults to Username.", ParameterSetName = "Synthetic")]
5268
public string? UserDisplayName { private get; set; }
5369

5470
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "UserID.", ParameterSetName = "UserData-RelyingParty")]
5571
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "UserID.", ParameterSetName = "UserData-HostData")]
5672
public byte[]? UserID { private get; set; }
5773

5874
// Parameters for credential configuration
59-
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challange.")]
60-
public required Challenge Challenge { private get; set; }
75+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserData-HostData")]
76+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserData-RelyingParty")]
77+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserEntity-HostData")]
78+
[Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Challenge for credential registration.", ParameterSetName = "UserEntity-RelyingParty")]
79+
public Challenge? Challenge { private get; set; }
6180

6281
[Parameter(Mandatory = false, ValueFromPipeline = false, HelpMessage = "Should this credential be discoverable.")]
6382
public bool Discoverable { private get; set; } = true;
@@ -103,6 +122,20 @@ protected override void ProcessRecord()
103122
// Set up key collector for PIN operations
104123
fido2Session.KeyCollector = YubiKeyModule._KeyCollector.YKKeyCollectorDelegate;
105124

125+
if (ParameterSetName == "Synthetic")
126+
{
127+
WriteDebug("Synthetic mode: generating Challenge and UserID automatically.");
128+
Challenge = FIDO2.Challenge.CreateSyntheticChallenge(RelyingPartyID);
129+
RelyingParty = new RelyingParty(RelyingPartyID) { Name = RelyingPartyName ?? RelyingPartyID };
130+
byte[] syntheticUserId = SyntheticCredentialHelper.GenerateUserID();
131+
WriteDebug($"Generated synthetic UserID: {Converter.ByteArrayToString(syntheticUserId)}");
132+
UserEntity = new UserEntity(syntheticUserId.AsMemory())
133+
{
134+
Name = Username,
135+
DisplayName = UserDisplayName ?? Username,
136+
};
137+
}
138+
106139
// Configure relying party information
107140
if (RelyingParty is null)
108141
{
@@ -142,7 +175,7 @@ protected override void ProcessRecord()
142175
{
143176
type = "webauthn.create",
144177
origin = $"https://{RelyingParty.Id}",
145-
challenge = Challenge.Base64URLEncode(),
178+
challenge = Challenge!.Base64URLEncode(),
146179
};
147180

148181
var clientDataJSON = System.Text.Json.JsonSerializer.Serialize(clientData);
@@ -160,10 +193,12 @@ protected override void ProcessRecord()
160193
}
161194

162195
// Create and return the credential
163-
WriteDebug($"Sending new credential data into SDK");
196+
WriteDebug($"Promting for touch to complete the credential creation...");
197+
Console.WriteLine("Touch the YubiKey...");
164198
MakeCredentialData returnvalue = fido2Session.MakeCredential(make);
165199

166200
var credData = new CredentialData(returnvalue, clientDataJSON, UserEntity!, RelyingParty);
201+
WriteInformation($"Credential created for {UserEntity!.DisplayName ?? UserEntity.Name} using RP: {RelyingParty.Id}.", new string[] { "FIDO2", "Info" });
167202
WriteObject(credData);
168203
}
169204
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <summary>
2+
/// Helpers for creating FIDO2 synthetic credentials without an IdP.
3+
/// <c>GenerateUserID</c> returns 32 cryptographically random bytes suitable for
4+
/// WebAuthn <c>user.id</c> (spec allows 1-64 bytes; value must not contain PII).
5+
/// </summary>
6+
7+
// Imports
8+
using Yubico.YubiKey.Cryptography;
9+
10+
namespace powershellYK.support.FIDO2
11+
{
12+
// Helpers for creating FIDO2 synthetic credentials without an IdP
13+
public static class SyntheticCredentialHelper
14+
{
15+
// Generates a 32-byte cryptographically random user ID
16+
public static byte[] GenerateUserID()
17+
{
18+
byte[] userId = new byte[32];
19+
var rng = CryptographyProviders.RngCreator();
20+
rng.GetBytes(userId);
21+
return userId;
22+
}
23+
}
24+
}

Module/types/FIDO2/Challenge.cs

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
/// Handles challenge generation, encoding, and conversion between formats.
44
///
55
/// .EXAMPLE
6-
/// # Create a challenge from a base64 string
6+
/// # Create a challenge from a base64 string (from an IdP registerBegin response)
77
/// $challenge = [powershellYK.FIDO2.Challenge]::new("SGVsbG8gV29ybGQ=")
88
/// Write-Host $challenge.ToString()
99
///
1010
/// .EXAMPLE
11-
/// # Create a fake challenge for testing
12-
/// $challenge = [powershellYK.FIDO2.Challenge]::FakeChallange("example.com")
11+
/// # Create a synthetic challenge locally (without an actual IdP)
12+
/// $challenge = [powershellYK.FIDO2.Challenge]::CreateSyntheticChallenge("example.local")
1313
/// Write-Host $challenge.Base64URLEncode()
1414
/// </summary>
1515

@@ -37,10 +37,16 @@ public Challenge(byte[] value)
3737
this._challenge = value;
3838
}
3939

40-
// Generates a fake challenge for testing purposes
40+
public static Challenge CreateSyntheticChallenge(string relyingPartyID)
41+
{
42+
return new Challenge(BuildSyntheticChallengeBytes(32));
43+
}
44+
45+
// Kept for backward compatibility; Pester tests and earlier scripts reference this name.
46+
[System.Obsolete("Use CreateSyntheticChallenge instead.")]
4147
public static Challenge FakeChallange(string relyingPartyID)
4248
{
43-
return new Challenge(BuildFakeClientDataHash(relyingPartyID));
49+
return CreateSyntheticChallenge(relyingPartyID);
4450
}
4551

4652
// Converts the challenge to a string representation
@@ -111,23 +117,12 @@ private static string AddMissingPadding(string base64)
111117
return base64;
112118
}
113119

114-
// Builds a fake client data hash for testing
115-
private static byte[] BuildFakeClientDataHash(string relyingPartyId)
120+
private static byte[] BuildSyntheticChallengeBytes(int length = 32)
116121
{
117-
// Convert relying party ID to bytes
118-
byte[] idBytes = System.Text.Encoding.Unicode.GetBytes(relyingPartyId);
119-
120-
// Generate random challenge bytes
121-
var randomObject = CryptographyProviders.RngCreator();
122-
byte[] randomBytes = new byte[16];
123-
randomObject.GetBytes(randomBytes);
124-
125-
// Create hash of random bytes and relying party ID
126-
var digester = CryptographyProviders.Sha256Creator();
127-
_ = digester.TransformBlock(randomBytes, 0, randomBytes.Length, null, 0);
128-
_ = digester.TransformFinalBlock(idBytes, 0, idBytes.Length);
129-
130-
return digester.Hash!;
122+
byte[] randomBytes = new byte[length];
123+
var rng = CryptographyProviders.RngCreator();
124+
rng.GetBytes(randomBytes);
125+
return randomBytes;
131126
}
132127

133128
#endregion // Support Methods

Module/types/FIDO2/CredentialData.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Runtime.CompilerServices;
2020
using System.Text;
2121
using Yubico.YubiKey.Fido2;
22+
using Yubico.YubiKey.Fido2.Cose;
2223

2324
namespace powershellYK.FIDO2
2425
{
@@ -28,6 +29,8 @@ public class CredentialData
2829
// Properties for accessing credential data
2930
public MakeCredentialData MakeCredentialData { get { return this._makeCredentialData; } }
3031
public string ClientDataJSON { get { return this._clientDataJSON; } }
32+
public CoseKey? PublicKey => _makeCredentialData.AuthenticatorData.CredentialPublicKey;
33+
public ReadOnlyMemory<byte>? CredentialId => _makeCredentialData.AuthenticatorData.CredentialId?.Id;
3134

3235
// Internal storage for credential components
3336
private readonly string _clientDataJSON;

Pester/310-FIDO2.tests.ps1

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ Describe "FIDO2 Tests" -Tag @("FIDO2") {
1010
{Connect-YubikeyFIDO2 -PIN (ConvertTo-SecureString -String "654321" -AsPlainText -Force)} | Should -Not -Throw
1111
{Set-YubikeyFIDO2PIN -OldPIN (ConvertTo-SecureString -String "654321" -AsPlainText -Force) -NewPIN (ConvertTo-SecureString -String "123456" -AsPlainText -Force)} | Should -Not -Throw
1212
}
13+
It -Name "Create synthetic credential (no IdP)" -Test {
14+
{New-YubiKeyFIDO2Credential -RelyingPartyID 'powershellYK-synthetic' -Username 'syntheticUser'} | Should -Not -Throw
15+
(Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic' }).UserName | Should -Be 'syntheticUser'
16+
{Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic' } | ForEach-Object { Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false }} | Should -Not -Throw
17+
}
18+
It -Name "Create synthetic credential with display name" -Test {
19+
{New-YubiKeyFIDO2Credential -RelyingPartyID 'powershellYK-synthetic2' -Username 'synUser2' -UserDisplayName 'Synthetic User Two'} | Should -Not -Throw
20+
{Get-YubiKeyFIDO2Credential | Where-Object { $_.RPId -eq 'powershellYK-synthetic2' } | ForEach-Object { Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false }} | Should -Not -Throw
21+
}
1322
It -Name "Clear all credentials" -Test {
1423
{Get-YubiKeyFIDO2Credential|%{Remove-YubikeyFIDO2Credential -CredentialId $_.CredentialID -Confirm:$false}} | Should -Not -Throw
1524
(Get-YubiKeyFIDO2Credential).Count | Should -Be 0

0 commit comments

Comments
 (0)