diff --git a/.gitignore b/.gitignore index 18f5e5a..7c8115e 100644 --- a/.gitignore +++ b/.gitignore @@ -365,3 +365,4 @@ FodyWeavers.xsd /powershellYK.psd1 /.cursorrules /Docs/Cookbook/Set-BIO-random-PIN.ps1 +/Docs/Cookbook/Enroll-FIDO2-On-Behalf-Of-Mock-IdP.ps1 diff --git a/Docs/Commands/Export-YubiKeyFIDO2Blob.md b/Docs/Commands/Export-YubiKeyFIDO2Blob.md new file mode 100644 index 0000000..d583f72 --- /dev/null +++ b/Docs/Commands/Export-YubiKeyFIDO2Blob.md @@ -0,0 +1,144 @@ +--- +document type: cmdlet +external help file: powershellYK.dll-Help.xml +HelpUri: '' +Locale: en-SE +Module Name: powershellYK +ms.date: 03-20-2026 +PlatyPS schema version: 2024-05-01 +title: Export-YubiKeyFIDO2Blob +--- + +# Export-YubiKeyFIDO2Blob + +## SYNOPSIS + +Exports large blob from YubiKey FIDO2 by Credential ID or Relying Party ID (Origin). + +## SYNTAX + +### Export LargeBlob + +``` +Export-YubiKeyFIDO2Blob -CredentialId -OutFile [] +``` + +### Export LargeBlob by RelyingPartyID + +``` +Export-YubiKeyFIDO2Blob -RelyingPartyID -OutFile [] +``` + +## ALIASES + +## DESCRIPTION + +Requires YubiKey firmware version 5.7.4 or later. + +## EXAMPLES + +### Example 1 + +```powershell +PS C:\> Export-YubiKeyFIDO2Blob -RelyingPartyID "powershellYK" -OutFile storedfile.txt +Touch the YubiKey... +``` + +Exports the large blob for the credential with the specified Credential ID to the specified output file. + +## PARAMETERS + +### -CredentialId + +Credential ID (hex or base64url string) to export large blob for. + +```yaml +Type: System.Nullable`1[powershellYK.FIDO2.CredentialID] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Export LargeBlob + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -OutFile + +Output file path for the exported large blob + +```yaml +Type: System.IO.FileInfo +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Export LargeBlob + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Export LargeBlob by RelyingPartyID + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -RelyingPartyID + +Relying Party ID (Origin), or relying party display name if unique, to export large blob for. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: +- RP +- Origin +ParameterSets: +- Name: Export LargeBlob by RelyingPartyID + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + +{{ Fill in the Description }} + +## NOTES + +{{ Fill in the Notes }} + +## RELATED LINKS + +[FIDO2 large blobs ("largeBlobs" option)](https://docs.yubico.com/yesdk/users-manual/application-fido2/large-blobs.html) + diff --git a/Docs/Commands/Import-YubiKeyFIDO2Blob.md b/Docs/Commands/Import-YubiKeyFIDO2Blob.md new file mode 100644 index 0000000..b5c60a2 --- /dev/null +++ b/Docs/Commands/Import-YubiKeyFIDO2Blob.md @@ -0,0 +1,174 @@ +--- +document type: cmdlet +external help file: powershellYK.dll-Help.xml +HelpUri: '' +Locale: en-SE +Module Name: powershellYK +ms.date: 03-20-2026 +PlatyPS schema version: 2024-05-01 +title: Import-YubiKeyFIDO2Blob +--- + +# Import-YubiKeyFIDO2Blob + +## SYNOPSIS + +Imports large blob to YubiKey FIDO2 by Credential ID or Relying Party ID (Origin). + +## SYNTAX + +### Set LargeBlob + +``` +Import-YubiKeyFIDO2Blob -LargeBlob -CredentialId [-Force] + [] +``` + +### Set LargeBlob by RelyingPartyID + +``` +Import-YubiKeyFIDO2Blob -LargeBlob -RelyingPartyID [-Force] [] +``` + +## ALIASES + +This cmdlet has the following aliases, + {{Insert list of aliases}} + +## DESCRIPTION + +Requires YubiKey firmware version 5.7 or later. + +## EXAMPLES + +### Example 1 + +```powershell +PS C:\> Import-YubiKeyFIDO2Blob -RelyingPartyID "powershellYK" -LargeBlob FileToImport.txt +Touch the YubiKey... +``` + +Imports the large blob from the specified file for the credential with the specified Relying Party ID (or display name, if unique) to the YubiKey. + +## PARAMETERS + +### -CredentialId + +Credential ID (hex or base64url string) to associate with the large blob array. + +```yaml +Type: System.Nullable`1[powershellYK.FIDO2.CredentialID] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Set LargeBlob + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Force + +Overwrite existing large blob entry for this credential without prompting. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Set LargeBlob + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Set LargeBlob by RelyingPartyID + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -LargeBlob + +File to import as large blob + +```yaml +Type: System.IO.FileInfo +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: +- Name: Set LargeBlob + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +- Name: Set LargeBlob by RelyingPartyID + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -RelyingPartyID + +Relying party ID, or relying party display name if unique, to associate with the large blob. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: +- RP +- Origin +ParameterSets: +- Name: Set LargeBlob by RelyingPartyID + Position: Named + IsRequired: true + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### System.Object + +{{ Fill in the Description }} + +## NOTES + +{{ Fill in the Notes }} + +## RELATED LINKS + +[FIDO2 large blobs ("largeBlobs" option)](https://docs.yubico.com/yesdk/users-manual/application-fido2/large-blobs.html) \ No newline at end of file diff --git a/Module/Cmdlets/FIDO2/ExportYubiKeyFIDO2Blob.cs b/Module/Cmdlets/FIDO2/ExportYubiKeyFIDO2Blob.cs new file mode 100644 index 0000000..1d85dbf --- /dev/null +++ b/Module/Cmdlets/FIDO2/ExportYubiKeyFIDO2Blob.cs @@ -0,0 +1,310 @@ +/// +/// 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´. +/// +/// .EXAMPLE +/// Export-YubiKeyFIDO2Blob -OutFile fileName.txt -RelyingPartyID "demo.yubico.com" +/// Exports a large blob to file when there is no more than one credential for the Relying Party on the YubiKey +/// +/// .EXAMPLE +/// Export-YubiKeyFIDO2Blob -OutFile fileName.txt -CredentialId "19448fe...67ab9207071e" +/// Exports a large blob to file for a specified FIDO2 Credential by ID (handles multiple entries for the same Relying Party) +/// + +// 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")] + 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 = "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; } + + // 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 exporting large blob + if (ParameterSetName == "Export LargeBlob" || ParameterSetName == "Export LargeBlob by RelyingPartyID") + { + 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 == "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 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 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" }); + } + } + } +} + diff --git a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2.cs b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2.cs index ed87aef..d089dc1 100644 --- a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2.cs +++ b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2.cs @@ -1,4 +1,4 @@ -/// +/// /// Retrieves information about the FIDO2 applet on a YubiKey. /// Returns details about supported features, capabilities, and current settings. /// Requires a YubiKey with FIDO2 support and administrator privileges on Windows. @@ -58,4 +58,4 @@ protected override void ProcessRecord() } } } -} +} \ No newline at end of file diff --git a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs index e5c8d1c..25e8f75 100644 --- a/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs +++ b/Module/Cmdlets/FIDO2/GetYubikeyFIDO2Credential.cs @@ -1,4 +1,4 @@ -/// +/// /// Retrieves FIDO2 credentials stored on a YubiKey. /// Lists all credentials or retrieves a specific credential by ID. /// Requires a YubiKey with FIDO2 support and administrator privileges on Windows. @@ -14,6 +14,10 @@ /// .EXAMPLE /// Get-YubiKeyFIDO2Credential -CredentialIdBase64Url "base64url_encoded_id" /// Retrieves a specific FIDO2 credential using its Base64URL encoded ID +/// +/// .EXAMPLE +/// Get-YubiKeyFIDO2Credential -RelyingPartyID "demo.yubico.com" +/// Lists credentials for a specific Relying Party ID (or Origin) /// // Imports @@ -39,6 +43,11 @@ public class GetYubikeyFIDO2CredentialsCommand : PSCmdlet [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Credential ID to remove int Base64 URL encoded format", ParameterSetName = "List-CredentialID-Base64URL")] public string? CredentialIdBase64Url { get; set; } = string.Empty; + [Parameter(Mandatory = true, ValueFromPipeline = false, HelpMessage = "Filter credentials by relying party ID", ParameterSetName = "List-RelyingPartyID")] + [Alias("RP", "Origin")] + [ValidateNotNullOrEmpty] + public string? RelyingPartyID { get; set; } + // Initialize processing and verify requirements protected override void BeginProcessing() { @@ -69,7 +78,9 @@ protected override void BeginProcessing() protected override void ProcessRecord() { // Convert Base64URL credential ID if provided - if (!this.CredentialID.HasValue && CredentialIdBase64Url is not null) + if (ParameterSetName == "List-CredentialID-Base64URL" && + !this.CredentialID.HasValue && + !string.IsNullOrWhiteSpace(CredentialIdBase64Url)) { this.CredentialID = powershellYK.FIDO2.CredentialID.FromStringBase64URL(CredentialIdBase64Url); } @@ -81,6 +92,12 @@ protected override void ProcessRecord() // Enumerate all relying parties var relyingParties = fido2Session.EnumerateRelyingParties(); + if (ParameterSetName == "List-RelyingPartyID") + { + relyingParties = relyingParties + .Where(rp => string.Equals(rp.Id, RelyingPartyID, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } if (!relyingParties.Any()) // Check if there are no relying parties { @@ -105,7 +122,13 @@ protected override void ProcessRecord() foreach (CredentialUserInfo user in relayCredentials) { - if (ParameterSetName == "List-All" || (user.CredentialId.Id.ToArray().SequenceEqual(this.CredentialID!.Value.ToByte()))) + bool includeCredential = + ParameterSetName == "List-All" || + ParameterSetName == "List-RelyingPartyID" || + (this.CredentialID.HasValue && + user.CredentialId.Id.ToArray().SequenceEqual(this.CredentialID.Value.ToByte())); + + if (includeCredential) { Credential credential = new Credential(relyingParty: relyingParty, credentialUserInfo: user); WriteObject(credential); diff --git a/Module/Cmdlets/FIDO2/Import-YubiKeyFIDO2Blob.cs b/Module/Cmdlets/FIDO2/Import-YubiKeyFIDO2Blob.cs new file mode 100644 index 0000000..97eae12 --- /dev/null +++ b/Module/Cmdlets/FIDO2/Import-YubiKeyFIDO2Blob.cs @@ -0,0 +1,344 @@ +/// +/// Allows uploading of large blobs to the YubiKey FIDO2 applet, associated with a specific credential ID or relying party. +/// Requires a YubiKey with FIDO2 support and administrator privileges on Windows. +/// +/// Sends minimum PIN length to specified relying party +/// +/// .EXAMPLE +/// Set-YubiKeyFIDO2 -LargeBlob test.txt -RelyingPartyID "demo.yubico.com" +/// Imports a file as a large blob when there is no more than one credential for the Relying Party on the YubiKey +/// +/// .EXAMPLE +/// Set-YubiKeyFIDO2 -LargeBlob test.txt -CredentialId "19448fe...67ab9207071e" +/// Imports a file as a large blob for a specified FIDO2 Credential by ID (handles multiple entries for the same Relying Party) +/// +/// .EXAMPLE +/// cd C:\CODE +/// Set-YubiKeyFIDO2 -LargeBlob test.txt -CredentialId "19448fe...67ab9207071e" -Force +/// Imports a file as a large blob and overwrites any existing blob entry for that credential without prompting +/// + +using Microsoft.VisualBasic; +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; +using System.Security.Cryptography; +using Yubico.YubiKey; +using Yubico.YubiKey.Cryptography; +using Yubico.YubiKey.Fido2; + +namespace powershellYK.Cmdlets.Fido +{ + [Cmdlet(VerbsData.Import, "YubiKeyFIDO2Blob")] + public class ImportYubikeyFIDO2BlobCmdlet : PSCmdlet + { + // Parameters for large blob import + [Parameter( + Mandatory = true, + ParameterSetName = "Set LargeBlob", + ValueFromPipeline = false, + HelpMessage = "File to import as large blob" + )] + [Parameter( + Mandatory = true, + ParameterSetName = "Set LargeBlob by RelyingPartyID", + ValueFromPipeline = false, + HelpMessage = "File to import as large blob" + )] + [TransformPath] + [ValidatePath(fileMustExist: true, fileMustNotExist: false)] + public required System.IO.FileInfo LargeBlob { get; set; } + + [Parameter( + Mandatory = true, + ParameterSetName = "Set LargeBlob", + ValueFromPipeline = false, + HelpMessage = "Credential ID (hex or base64url string) to associate with the large blob array." + )] + public powershellYK.FIDO2.CredentialID? CredentialId { get; set; } + + [Parameter( + Mandatory = true, + ParameterSetName = "Set LargeBlob by RelyingPartyID", + ValueFromPipeline = false, + HelpMessage = "Relying party ID, or relying party display name if unique, to associate with the large blob." + )] + [Alias("RP", "Origin")] + [ValidateNotNullOrEmpty] + public string? RelyingPartyID { get; set; } + + [Parameter( + Mandatory = false, + ParameterSetName = "Set LargeBlob", + ValueFromPipeline = false, + HelpMessage = "Overwrite existing large blob entry for this credential without prompting." + )] + [Parameter( + Mandatory = false, + ParameterSetName = "Set LargeBlob by RelyingPartyID", + ValueFromPipeline = false, + HelpMessage = "Overwrite existing large blob entry for this credential without prompting." + )] + public SwitchParameter Force { get; set; } + + // 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 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; + + switch (ParameterSetName) + { + case "Set LargeBlob": + case "Set LargeBlob by RelyingPartyID": + // 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)."); + + if (LargeBlob is null) + { + throw new ArgumentException("You must enter a valid file path.", nameof(LargeBlob)); + } + + // Resolve and read the input file + string resolvedPath = GetUnresolvedProviderPathFromPSPath(LargeBlob.FullName); + byte[] blobData; + try + { + blobData = System.IO.File.ReadAllBytes(resolvedPath); + WriteDebug($"Step 2: Input file loaded from '{LargeBlob.FullName}' ({blobData.Length} bytes)."); + } + catch (Exception ex) + { + throw new IOException($"Failed to read large blob data from file '{LargeBlob}'.", ex); + } + + // Resolve target credential and corresponding relying party. + RelyingParty? credentialRelyingParty = null; + var relyingParties = fido2Session.EnumerateRelyingParties(); + powershellYK.FIDO2.CredentialID selectedCredentialId; + if (ParameterSetName == "Set LargeBlob by RelyingPartyID") + { + if (string.IsNullOrWhiteSpace(RelyingPartyID)) + { + throw new ArgumentNullException(nameof(RelyingPartyID), "A relying party ID/name must be provided when setting 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 credentialsForOrigin = fido2Session.EnumerateCredentialsForRelyingParty(credentialRelyingParty); + if (credentialsForOrigin.Count == 0) + { + throw new InvalidOperationException($"No credentials found for relying party '{credentialRelyingParty.Id}'."); + } + if (credentialsForOrigin.Count > 1) + { + string candidateCredentialIds = string.Join(", ", + credentialsForOrigin.Select(c => Convert.ToHexString(c.CredentialId.Id.ToArray()).ToLowerInvariant())); + throw new InvalidOperationException( + $"Relying party '{credentialRelyingParty.Id}' has multiple credentials ({credentialsForOrigin.Count}). " + + $"Use Get-YubiKeyFIDO2Credential -RelyingPartyID {credentialRelyingParty.Id} to list credentials, then use -CredentialId to choose which credential to use."); + } + + selectedCredentialId = (powershellYK.FIDO2.CredentialID)credentialsForOrigin[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 setting 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 3: 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 clientDataHash = digester.Hash!.AsMemory(); + WriteDebug($"Step 4: 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 5: 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 6: Current large blob array loaded ({blobArray.Entries.Count} entries)."); + + // Enforce one entry per credential key by detecting existing decryptable entries. + var matchingEntryIndexes = new List(); + for (int i = 0; i < blobArray.Entries.Count; i++) + { + if (blobArray.Entries[i].TryDecrypt(retrievedKey.Value, out _)) + { + matchingEntryIndexes.Add(i); + } + } + + if (matchingEntryIndexes.Count > 0) + { + string existingMsg = + $"Found {matchingEntryIndexes.Count} existing large blob entr{(matchingEntryIndexes.Count == 1 ? "y" : "ies")} " + + $"for relying party '{credentialRelyingParty.Id}'."; + WriteWarning(existingMsg); + + bool overwriteExisting = Force.IsPresent; + if (!overwriteExisting) + { + overwriteExisting = ShouldContinue( + $"{existingMsg} Overwrite existing entr{(matchingEntryIndexes.Count == 1 ? "y" : "ies")}?", + "Large blob entry already exists"); + } + + if (!overwriteExisting) + { + WriteWarning("Operation cancelled by user. Existing large blob entries were left unchanged."); + return; + } + + for (int i = matchingEntryIndexes.Count - 1; i >= 0; i--) + { + blobArray.RemoveEntry(matchingEntryIndexes[i]); + } + } + + WriteDebug($"Step 7: Adding blob entry ({blobData.Length} bytes)."); + // Add a new encrypted entry, binding the data to the retrieved largeBlobKey + blobArray.AddEntry(blobData, retrievedKey.Value); + + WriteDebug("Step 8: Writing updated large blob array to YubiKey..."); + // Write the updated Large Blob array back to the authenticator + fido2Session.SetSerializedLargeBlobArray(blobArray); + + WriteInformation( + $"FIDO2 large blob entry added successfully for Relying Party (Origin): '{credentialRelyingParty.Id}'.", + new[] { "FIDO2", "LargeBlob" }); + break; + } + } + } + } +} diff --git a/Module/Cmdlets/FIDO2/SetYubikeyFIDO2.cs b/Module/Cmdlets/FIDO2/SetYubikeyFIDO2.cs index 46fa2e7..f763961 100644 --- a/Module/Cmdlets/FIDO2/SetYubikeyFIDO2.cs +++ b/Module/Cmdlets/FIDO2/SetYubikeyFIDO2.cs @@ -253,4 +253,4 @@ protected override void ProcessRecord() } } } -} +} \ No newline at end of file diff --git a/Module/Cmdlets/PIV/GetYubikeyPIV.cs b/Module/Cmdlets/PIV/GetYubikeyPIV.cs index 82c64b8..75ad471 100644 --- a/Module/Cmdlets/PIV/GetYubikeyPIV.cs +++ b/Module/Cmdlets/PIV/GetYubikeyPIV.cs @@ -111,14 +111,14 @@ protected override void ProcessRecord() // Get supported algorithms List supportedAlgorithms = new List(); - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa1024)) { supportedAlgorithms.Add("Rsa1024"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa2048)) { supportedAlgorithms.Add("Rsa2048"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa3072)) { supportedAlgorithms.Add("Rsa3072"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa4096)) { supportedAlgorithms.Add("Rsa4096"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivEccP256)) { supportedAlgorithms.Add("EcP256"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivEccP384)) { supportedAlgorithms.Add("EcP384"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivCurve25519)) { supportedAlgorithms.Add("Ed25519"); }; - if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivCurve25519)) { supportedAlgorithms.Add("X25519"); }; + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa1024)) { supportedAlgorithms.Add("Rsa1024"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa2048)) { supportedAlgorithms.Add("Rsa2048"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa3072)) { supportedAlgorithms.Add("Rsa3072"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivRsa4096)) { supportedAlgorithms.Add("Rsa4096"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivEccP256)) { supportedAlgorithms.Add("EcP256"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivEccP384)) { supportedAlgorithms.Add("EcP384"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivCurve25519)) { supportedAlgorithms.Add("Ed25519"); } + if (((YubiKeyDevice)YubiKeyModule._yubikey!).HasFeature(YubiKeyFeature.PivCurve25519)) { supportedAlgorithms.Add("X25519"); } // Get CHUID information CardholderUniqueId chuid; diff --git a/Module/powershellYK.psd1 b/Module/powershellYK.psd1 index a6f1d7d..de1641d 100644 --- a/Module/powershellYK.psd1 +++ b/Module/powershellYK.psd1 @@ -86,6 +86,7 @@ CmdletsToExport = @( 'Export-YubiKeyFIDO2Blob', 'Get-YubiKeyFIDO2', 'Get-YubiKeyFIDO2Credential', + 'Import-YubiKeyFIDO2Blob', 'New-YubiKeyFIDO2Credential', 'Remove-YubiKeyFIDO2Credential' 'Set-YubiKeyFIDO2', diff --git a/Pester/290-Confirm-YubikeyAttestion.tests.ps1 b/Pester/290-Confirm-YubikeyAttestion.tests.ps1 index 526cfbf..13f7fa8 100644 --- a/Pester/290-Confirm-YubikeyAttestion.tests.ps1 +++ b/Pester/290-Confirm-YubikeyAttestion.tests.ps1 @@ -49,12 +49,12 @@ $pest_input = [byte[]](0x30, 0x82, 0x6, 0xEC, 0x30, 0x82, 0x6, 0x72, 0x2, 0x1, 0 } -Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates CSPN" -Tag 'Dry' { +Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates CSPN" -Tag 'Without-YubiKey' { BeforeEach -Scriptblock { } It -Name "Verify Files" -Test { - $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot\TestData\piv_attestion_cspn_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot\TestData\piv_attestion_cspn_intermediatecertificate.cer" + $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot/TestData/piv_attestion_cspn_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot/TestData/piv_attestion_cspn_intermediatecertificate.cer" $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a $pest_return.isFIPSSeries | Should -BeFalse @@ -64,12 +64,12 @@ Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates CSPN" } } -Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates FIPS" -Tag 'Dry' { +Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates FIPS" -Tag 'Without-YubiKey' { BeforeEach -Scriptblock { } It -Name "Verify '-AttestationCertificate _x509_ -IntermediateCertificate _x509_' works" -Test { - $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot\TestData\piv_attestion_fips_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot\TestData\piv_attestion_fips_intermediatecertificate.cer" + $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot/TestData/piv_attestion_fips_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot/TestData/piv_attestion_fips_intermediatecertificate.cer" $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a $pest_return.isFIPSSeries | Should -BeTrue @@ -79,13 +79,13 @@ Describe "Confirm-YubikeyAttestation Attestation/Intermediate Certificates FIPS" } } -Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { +Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Without-YubiKey' { BeforeEach -Scriptblock { $pest_return = $Null } It -Name "Verify 'JustAttestCertificate-File' works" -Test { - $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot\TestData\piv_attestion_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot\TestData\piv_attestion_intermediatecertificate.cer" + $pest_return = Confirm-YubikeyAttestation -AttestationCertificateFile "$PSScriptRoot/TestData/piv_attestion_attestioncertificate.cer" -IntermediateCertificateFile "$PSScriptRoot/TestData/piv_attestion_intermediatecertificate.cer" $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a $pest_return.isFIPSSeries | Should -BeFalse @@ -95,8 +95,8 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } It -Name "Verify 'JustAttestCertificate-Object' works" -Test { - $pest_att = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot\TestData\piv_attestion_attestioncertificate.cer") - $pest_int = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot\TestData\piv_attestion_intermediatecertificate.cer") + $pest_att = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot/TestData/piv_attestion_attestioncertificate.cer") + $pest_int = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot/TestData/piv_attestion_intermediatecertificate.cer") $pest_return = Confirm-YubikeyAttestation -AttestationCertificate $pest_att -IntermediateCertificate $pest_int $pest_return | Should -BeOfType powershellYK.Attestation @@ -106,7 +106,7 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } It -Name "Verify 'requestWithBuiltinAttestation-File' works" -Test { - $pest_return = Confirm-YubikeyAttestation -CertificateRequestFile "$PSScriptRoot\TestData\piv_attestion_certificaterequest_with_attestion.req" + $pest_return = Confirm-YubikeyAttestation -CertificateRequestFile "$PSScriptRoot/TestData/piv_attestion_certificaterequest_with_attestion.req" $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a $pest_return.AttestationValidated | Should -Be $True @@ -114,7 +114,7 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } It -Name "Verify 'requestWithBuiltinAttestation-Object' works" -Test { - $pest_req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::LoadSigningRequestPem((Get-Content "$PSScriptRoot\TestData\piv_attestion_certificaterequest_with_attestion.req"),[System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.X509Certificates.CertificateRequestLoadOptions]::UnsafeLoadCertificateExtensions) + $pest_req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::LoadSigningRequestPem((Get-Content "$PSScriptRoot/TestData/piv_attestion_certificaterequest_with_attestion.req"),[System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.X509Certificates.CertificateRequestLoadOptions]::UnsafeLoadCertificateExtensions) $pest_return = Confirm-YubikeyAttestation -CertificateRequest $pest_req $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a @@ -123,7 +123,7 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } It -Name "Verify 'requestWithExternalAttestation-File' works" -Test { - $pest_return = Confirm-YubikeyAttestation -CertificateRequestFile "$PSScriptRoot\TestData\piv_attestion_5_4_3_9a_request.req" -AttestationCertificateFile "$PSScriptRoot\TestData\piv_attestion_5_4_3_9a_slot_attestation.cer" -IntermediateCertificateFile "$PSScriptRoot\TestData\piv_attestion_5_4_3_9a_AttestationIntermediateCertificate.cer" + $pest_return = Confirm-YubikeyAttestation -CertificateRequestFile "$PSScriptRoot/TestData/piv_attestion_5_4_3_9a_request.req" -AttestationCertificateFile "$PSScriptRoot/TestData/piv_attestion_5_4_3_9a_slot_attestation.cer" -IntermediateCertificateFile "$PSScriptRoot/TestData/piv_attestion_5_4_3_9a_AttestationIntermediateCertificate.cer" $pest_return | Should -BeOfType powershellYK.Attestation $pest_return.Slot | Should -Be 0x9a @@ -132,9 +132,9 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } It -Name "Verify 'requestWithExternalAttestation-Object' works" -Test { - $pest_req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::LoadSigningRequestPem((Get-Content "$PSScriptRoot\TestData\piv_attestion_certificaterequest_with_attestion.req"),[System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.X509Certificates.CertificateRequestLoadOptions]::UnsafeLoadCertificateExtensions) - $pest_att = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot\TestData\piv_attestion_attestioncertificate.cer") - $pest_int = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot\TestData\piv_attestion_intermediatecertificate.cer") + $pest_req = [System.Security.Cryptography.X509Certificates.CertificateRequest]::LoadSigningRequestPem((Get-Content "$PSScriptRoot/TestData/piv_attestion_certificaterequest_with_attestion.req"),[System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.X509Certificates.CertificateRequestLoadOptions]::UnsafeLoadCertificateExtensions) + $pest_att = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot/TestData/piv_attestion_attestioncertificate.cer") + $pest_int = [System.Security.Cryptography.X509Certificates.X509Certificate2]::New("$PSScriptRoot/TestData/piv_attestion_intermediatecertificate.cer") $pest_return = Confirm-YubikeyAttestation -CertificateRequest $pest_req -AttestationCertificate $pest_att -IntermediateCertificate $pest_int $pest_return | Should -BeOfType powershellYK.Attestation @@ -145,7 +145,7 @@ Describe "Confirm-YubikeyAttestation ParameterSetName tests" -Tag 'Dry' { } -Describe "Confirm-YubikeyAttestation Errors" -Tag 'Dry' { +Describe "Confirm-YubikeyAttestation Errors" -Tag 'Without-YubiKey' { It -Name "Incorrect string on CertificateRequest" -Test { {Confirm-YubikeyAttestation -CertificateRequest ""} | Should -Throw } diff --git a/Pester/315-Confirm-YubiKeyFIDO2Attestation.tests.ps1 b/Pester/315-Confirm-YubiKeyFIDO2Attestation.tests.ps1 index 296ed61..9c99a6e 100644 --- a/Pester/315-Confirm-YubiKeyFIDO2Attestation.tests.ps1 +++ b/Pester/315-Confirm-YubiKeyFIDO2Attestation.tests.ps1 @@ -27,16 +27,16 @@ Describe "Confirm-YubiKeyFIDO2Attestation paths" -Tag "Without-YubiKey" { } } -Describe "Confirm-YubiKeyFIDO2Attestation output" -Tag 'Dry' { +Describe "Confirm-YubiKeyFIDO2Attestation output" -Tag 'Without-YubiKey' { It -Name "Verify AttestationPath contains Yubico root" -Test { - $pest_return = Confirm-YubiKeyFIDO2Attestation -AttestationObject "$PSScriptRoot\TestData\attestation.bin" + $pest_return = Confirm-YubiKeyFIDO2Attestation -AttestationObject "$PSScriptRoot/TestData/attestation.bin" ($pest_return.AttestationPath -join ' ') | Should -Match 'Yubico' } } -Describe "Confirm-YubiKeyFIDO2Attestation Errors" -Tag 'Dry' { +Describe "Confirm-YubiKeyFIDO2Attestation Errors" -Tag 'Without-YubiKey' { It -Name "Missing file throws" -Test { - $badPath = Join-Path $PSScriptRoot "TestData\nonexistent_attestation_315_test.bin" + $badPath = Join-Path $PSScriptRoot "TestData/nonexistent_attestation_315_test.bin" (Test-Path $badPath) | Should -Be $false $threw = $false try { Confirm-YubiKeyFIDO2Attestation -AttestationObject $badPath } catch { $threw = $true } @@ -45,7 +45,7 @@ Describe "Confirm-YubiKeyFIDO2Attestation Errors" -Tag 'Dry' { It -Name "Invalid format throws" -Test { $threw = $false - try { Confirm-YubiKeyFIDO2Attestation -AttestationObject "$PSScriptRoot\TestData\rsa_2048_cert.pem" } catch { $threw = $true } + try { Confirm-YubiKeyFIDO2Attestation -AttestationObject "$PSScriptRoot/TestData/rsa_2048_cert.pem" } catch { $threw = $true } $threw | Should -Be $true } } diff --git a/Pester/320-FIDO2Blob.tests.ps1 b/Pester/320-FIDO2Blob.tests.ps1 new file mode 100644 index 0000000..642a556 --- /dev/null +++ b/Pester/320-FIDO2Blob.tests.ps1 @@ -0,0 +1,13 @@ +Describe "FIDO2 Blob Tests" -Tag @("FIDO2",'FIDO2Blob') { + BeforeAll { + { Connect-YubiKey } | Should -Not -Throw + { Connect-YubiKeyFIDO2 -PIN (ConvertTo-SecureString -String '123456' -AsPlainText -Force) } | Should -Not -Throw + { New-YubiKeyFIDO2Credential -RelyingPartyID 'powershellYK-FIDO2-BLOB' -Challenge ([powershellYK.FIDO2.Challenge]::FakeChallange("powershellYK")) -Discoverable:$true -Username 'powershellYKUser' -UserID 0x01 } | Should -Not -Throw + } + AfterAll { + Remove-YubikeyFIDO2Credential -RelayingParty 'powershellYK-FIDO2-BLOB' -Username powershellYKUser + } + It -Name "Store file in FIDO2 Blob" -Test { + { Import-YubiKeyFIDO2Blob -RelyingPartyID 'powershellYK-FIDO2-BLOB' -LargeBlob ".\Pester\TestData\piv_attestion_5_4_3_9a_request.req" } | Should -Not -Throw + } +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index f4a70dd..1dbcef5 100644 --- a/build.ps1 +++ b/build.ps1 @@ -4,11 +4,7 @@ if (Test-Path 'release') { $Directory = New-Item -Type Directory 'release' dotnet publish module --nologo --framework 'net8.0' --output "$($Directory.fullname)" -dotnet publish powershellYK_loader --nologo --framework 'net8.0' --output "$($Directory.fullname)\loader" - -Copy-Item "$($Directory.fullname)\loader\powershellYK_loader.dll" "$($Directory.fullname)" #Copy-Item "$($Directory.fullname)\loader\powershellYK_loader.pdb" "$($Directory.fullname)\module" -Remove-Item -Recurse "$($Directory.fullname)\loader" #Move-Item "$($Directory.fullname)\module\powershellYK.psd1" "$($Directory.fullname)" #Move-Item "$($Directory.fullname)\module\powershellYK.format.ps1xml" "$($Directory.fullname)"