-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathExportYubiKeyFIDO2Blob.cs
More file actions
348 lines (316 loc) · 17.1 KB
/
ExportYubiKeyFIDO2Blob.cs
File metadata and controls
348 lines (316 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/// <summary>
/// Allows the return of a large blob associated with a FIDO2 credential, which may contain additional metadata or state information for that credential.
/// Requires a YubiKey with FIDO2 support and administrator privileges on Windows.
/// When no credential or relying party is provided, the cmdlet automatically looks up the "blob-storage" credential.
///
/// .EXAMPLE
/// Export-YubiKeyFIDO2Blob -OutFile fileName.txt
/// Exports the large blob stored under the "blob-storage" credential.
///
/// .EXAMPLE
/// Export-YubiKeyFIDO2Blob -RelyingPartyID "demo.yubico.com" -OutFile fileName.txt
/// Exports a large blob to a file when there is no more than one credential for the relying party on the YubiKey.
///
/// .EXAMPLE
/// Export-YubiKeyFIDO2Blob -CredentialId "19448fe...67ab9207071e" -OutFile fileName.txt
/// Exports a large blob to a file for a specified FIDO2 credential by ID (use when the RP has multiple credentials).
/// </summary>
// Imports
using Newtonsoft.Json;
using powershellYK.FIDO2;
using powershellYK.support;
using powershellYK.support.transform;
using powershellYK.support.validators;
using System.Management.Automation; // Windows PowerShell namespace.
using System.Security.Cryptography;
using Yubico.YubiKey;
using Yubico.YubiKey.Cryptography;
using Yubico.YubiKey.Fido2;
namespace powershellYK.Cmdlets.Fido
{
[Cmdlet(VerbsData.Export, "YubiKeyFIDO2Blob", DefaultParameterSetName = "AutoLookup")]
public class ExportYubikeyFIDO2BlobCmdlet : PSCmdlet
{
[Parameter(
Mandatory = true,
ParameterSetName = "Export LargeBlob",
ValueFromPipeline = false,
HelpMessage = "Credential ID (hex or base64url string) to export large blob for."
)]
public powershellYK.FIDO2.CredentialID? CredentialId { get; set; }
[Parameter(
Mandatory = true,
ParameterSetName = "Export LargeBlob by RelyingPartyID",
ValueFromPipeline = false,
HelpMessage = "Relying Party ID (Origin), or relying party display name if unique, to export large blob for."
)]
[Alias("RP", "Origin")]
[ValidateNotNullOrEmpty]
public string? RelyingPartyID { get; set; }
[Parameter(
Mandatory = true,
ParameterSetName = "AutoLookup",
ValueFromPipeline = false,
HelpMessage = "Output file path for the exported large blob"
)]
[Parameter(
Mandatory = true,
ParameterSetName = "Export LargeBlob",
ValueFromPipeline = false,
HelpMessage = "Output file path for the exported large blob"
)]
[Parameter(
Mandatory = true,
ParameterSetName = "Export LargeBlob by RelyingPartyID",
ValueFromPipeline = false,
HelpMessage = "Output file path for the exported large blob"
)]
[TransformPath]
[ValidatePath(fileMustExist: false, fileMustNotExist: true)]
public required System.IO.FileInfo OutFile { get; set; }
private const string AutoCreateRpId = "blob-storage";
// Initialize processing and verify requirements
protected override void BeginProcessing()
{
// Check if running as Administrator
if (Windows.IsRunningAsAdministrator() == false)
{
throw new Exception("FIDO access on Windows requires running as Administrator.");
}
// Connect to YubiKey if not already connected
if (YubiKeyModule._yubikey is null)
{
WriteDebug("No YubiKey selected, calling Connect-Yubikey...");
var myPowersShellInstance = PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand("Connect-Yubikey");
if (this.MyInvocation.BoundParameters.ContainsKey("InformationAction"))
{
myPowersShellInstance = myPowersShellInstance.AddParameter("InformationAction", this.MyInvocation.BoundParameters["InformationAction"]);
}
myPowersShellInstance.Invoke();
WriteDebug($"Successfully connected");
}
// Connect to FIDO2 if not already authenticated
if (YubiKeyModule._fido2PIN is null)
{
WriteDebug("No FIDO2 session has been authenticated, calling Connect-YubikeyFIDO2...");
var myPowersShellInstance = PowerShell.Create(RunspaceMode.CurrentRunspace).AddCommand("Connect-YubikeyFIDO2");
if (this.MyInvocation.BoundParameters.ContainsKey("InformationAction"))
{
myPowersShellInstance = myPowersShellInstance.AddParameter("InformationAction", this.MyInvocation.BoundParameters["InformationAction"]);
}
myPowersShellInstance.Invoke();
if (YubiKeyModule._fido2PIN is null)
{
throw new Exception("Connect-YubikeyFIDO2 failed to connect to the FIDO2 applet!");
}
}
}
// Process the main cmdlet logic
protected override void ProcessRecord()
{
using (var fido2Session = new Fido2Session((YubiKeyDevice)YubiKeyModule._yubikey!))
{
fido2Session.KeyCollector = YubiKeyModule._KeyCollector.YKKeyCollectorDelegate;
// Verify the YubiKey supports large blobs
if (fido2Session.AuthenticatorInfo.MaximumSerializedLargeBlobArray is null)
{
throw new NotSupportedException("This YubiKey does not support FIDO2 large blobs.");
}
WriteDebug($"Step 1: Large blob support verified (max {fido2Session.AuthenticatorInfo.MaximumSerializedLargeBlobArray.Value} bytes).");
// Resolve target credential and corresponding relying party.
RelyingParty? credentialRelyingParty = null;
var relyingParties = fido2Session.EnumerateRelyingParties();
powershellYK.FIDO2.CredentialID selectedCredentialId;
if (ParameterSetName == "AutoLookup")
{
var match = relyingParties.FirstOrDefault(rp =>
string.Equals(rp.Id, AutoCreateRpId, StringComparison.OrdinalIgnoreCase));
if (match is null)
{
throw new InvalidOperationException(
$"No '{AutoCreateRpId}' credential found on this YubiKey. " +
"Use Import-YubiKeyFIDO2Blob to store a blob first, or specify -CredentialId / -RelyingPartyID.");
}
try
{
var creds = fido2Session.EnumerateCredentialsForRelyingParty(match);
if (creds.Count == 0)
{
throw new InvalidOperationException(
$"No credentials found for relying party '{match.Id}'.");
}
credentialRelyingParty = match;
selectedCredentialId = (powershellYK.FIDO2.CredentialID)creds[0].CredentialId;
}
catch (NotSupportedException)
{
throw new InvalidOperationException(
$"Unable to enumerate credentials for relying party '{match.Id}' due to unsupported algorithm.");
}
}
else if (ParameterSetName == "Export LargeBlob by RelyingPartyID")
{
if (string.IsNullOrWhiteSpace(RelyingPartyID))
{
throw new ArgumentNullException(nameof(RelyingPartyID), "A relying party ID/name must be provided when exporting a large blob by RelyingPartyID.");
}
var matchingRps = relyingParties.Where(rpMatch =>
string.Equals(rpMatch.Id, RelyingPartyID, StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrWhiteSpace(rpMatch.Name) && string.Equals(rpMatch.Name, RelyingPartyID, StringComparison.OrdinalIgnoreCase)))
.ToList();
if (matchingRps.Count == 0)
{
throw new ArgumentException($"No relying party found matching '{RelyingPartyID}' on this YubiKey.", nameof(RelyingPartyID));
}
if (matchingRps.Count > 1)
{
string rpCandidates = string.Join(", ", matchingRps.Select(rpMatch => $"'{rpMatch.Id}'"));
throw new InvalidOperationException(
$"Multiple relying parties matched '{RelyingPartyID}': {rpCandidates}. " +
"Use a specific RP ID with -RelyingPartyID, or specify -CredentialId directly.");
}
credentialRelyingParty = matchingRps[0];
try
{
var credentialsForRp = fido2Session.EnumerateCredentialsForRelyingParty(credentialRelyingParty);
if (credentialsForRp.Count == 0)
{
throw new InvalidOperationException($"No credentials found for relying party '{credentialRelyingParty.Id}'.");
}
if (credentialsForRp.Count > 1)
{
string candidateCredentialIds = string.Join(", ",
credentialsForRp.Select(c => Convert.ToHexString(c.CredentialId.Id.ToArray()).ToLowerInvariant()));
throw new InvalidOperationException(
$"Relying party '{credentialRelyingParty.Id}' has multiple credentials ({credentialsForRp.Count}). " +
$"Use Get-YubiKeyFIDO2Credential -RelyingPartyID {credentialRelyingParty.Id} to list credentials, then use -CredentialId to choose which credential to export.");
}
selectedCredentialId = (powershellYK.FIDO2.CredentialID)credentialsForRp[0].CredentialId;
}
catch (NotSupportedException)
{
throw new InvalidOperationException(
$"Unable to enumerate credentials for relying party '{credentialRelyingParty.Id}' due to unsupported algorithm.");
}
}
else
{
// Ensure a credential ID was supplied
if (CredentialId is null)
{
throw new ArgumentNullException(nameof(CredentialId), "A FIDO2 credential ID must be provided when exporting a large blob.");
}
selectedCredentialId = CredentialId.Value;
byte[] credentialIdBytes = selectedCredentialId.ToByte();
foreach (RelyingParty currentRp in relyingParties)
{
try
{
var credentials = fido2Session.EnumerateCredentialsForRelyingParty(currentRp);
foreach (var credInfo in credentials)
{
if (credInfo.CredentialId.Id.ToArray().SequenceEqual(credentialIdBytes))
{
credentialRelyingParty = currentRp;
break;
}
}
if (credentialRelyingParty is not null)
{
break;
}
}
catch (NotSupportedException)
{
// Skip relying parties with unsupported algorithms
continue;
}
}
if (credentialRelyingParty is null)
{
throw new ArgumentException($"Credential with ID '{selectedCredentialId}' not found on this YubiKey.", nameof(CredentialId));
}
}
WriteDebug($"Step 2: Target resolved to RP '{credentialRelyingParty.Id}' and credential '{selectedCredentialId}'.");
// Create client data hash for GetAssertion
byte[] challengeBytes = new byte[32];
RandomNumberGenerator.Fill(challengeBytes);
var clientData = new
{
type = "webauthn.get",
origin = $"https://{credentialRelyingParty.Id}",
challenge = Convert.ToBase64String(challengeBytes)
};
var clientDataJSON = JsonConvert.SerializeObject(clientData);
var clientDataBytes = System.Text.Encoding.UTF8.GetBytes(clientDataJSON);
var digester = CryptographyProviders.Sha256Creator();
_ = digester.TransformFinalBlock(clientDataBytes, 0, clientDataBytes.Length);
ReadOnlyMemory<byte> clientDataHash = digester.Hash!.AsMemory();
WriteDebug($"Step 3: Client data hash created for origin '{clientData.origin}'.");
// Perform GetAssertion to retrieve the largeBlobKey
var gaParams = new GetAssertionParameters(credentialRelyingParty, clientDataHash);
// Add the credential ID to the allow list (for non-resident keys)
gaParams.AllowCredential(selectedCredentialId.ToYubicoFIDO2CredentialID());
// Request the largeBlobKey extension
gaParams.AddExtension(Extensions.LargeBlobKey, new byte[] { 0xF5 });
// Execute assertion ceremony
Console.WriteLine("Touch the YubiKey...");
var assertions = fido2Session.GetAssertions(gaParams);
if (assertions.Count == 0)
{
throw new InvalidOperationException("GetAssertion returned no assertions.");
}
// Retrieve the per-credential largeBlobKey
var retrievedKey = assertions[0].LargeBlobKey;
if (retrievedKey is null)
{
throw new NotSupportedException("The credential does not support large blob keys. The credential may need to be recreated with the largeBlobKey extension.");
}
WriteDebug($"Step 4: Assertion completed and largeBlobKey retrieved ({assertions.Count} assertion(s)).");
// Get the current serialized Large Blob array from the authenticator
var blobArray = fido2Session.GetSerializedLargeBlobArray();
WriteDebug($"Step 5: Current large blob array loaded ({blobArray.Entries.Count} entries).");
byte[]? blobData = null;
int matchingEntryCount = 0;
int selectedEntryIndex = -1;
// Iterate entries and decrypt with this credential's largeBlobKey.
// If multiple entries match, pick the newest (highest index).
for (int i = 0; i < blobArray.Entries.Count; i++)
{
if (blobArray.Entries[i].TryDecrypt(retrievedKey.Value, out Memory<byte> decrypted))
{
matchingEntryCount++;
blobData = decrypted.ToArray();
selectedEntryIndex = i;
}
}
if (matchingEntryCount == 0 || blobData is null)
{
throw new InvalidOperationException($"No large blob entry found for credential '{selectedCredentialId}'.");
}
if (matchingEntryCount > 1)
{
WriteWarning(
$"Found {matchingEntryCount} large blob entries for credential '{selectedCredentialId}'. " +
$"Using newest entry at index {selectedEntryIndex}. " +
"Use Set-YubiKeyFIDO2 -LargeBlob and choose overwrite to compact to a single entry.");
}
WriteDebug($"Step 6: Blob entry selected from index {selectedEntryIndex} ({blobData.Length} bytes).");
WriteDebug($"Step 7: Writing blob data to '{OutFile.FullName}'.");
// Write the blob data to the output file
string resolvedPath = GetUnresolvedProviderPathFromPSPath(OutFile.FullName);
try
{
System.IO.File.WriteAllBytes(resolvedPath, blobData);
}
catch (Exception ex)
{
throw new IOException($"Failed to write large blob data to file '{OutFile}'.", ex);
}
WriteInformation(
$"FIDO2 large blob exported successfully for Relying Party (Origin): '{credentialRelyingParty.Id}'.",
new[] { "FIDO2", "LargeBlob" });
}
}
}
}