DSInternals.Passkeys.Core.psm1

<#
.SYNOPSIS
Creates a new WebAuthn credential by driving the local authenticator.
 
.DESCRIPTION
Calls the Windows WebAuthn API to make a credential according to the provided PublicKeyCredentialCreationOptions, triggering the system passkey UI. Returns the resulting AttestationPublicKeyCredential, which can be wrapped into a provider-specific attestation response and submitted to Microsoft Entra ID, Okta, or any other relying party.
 
.PARAMETER Options
The WebAuthn public key credential creation options. Returned directly by Get-EntraPasskeyRegistrationOptions.
 
.PARAMETER HostName
Optional host name used to derive the WebAuthn origin when the server-issued options omit the relying party identifier.
 
.PARAMETER OktaOptions
The Okta-specific credential creation options. Returned by Get-OktaPasskeyRegistrationOptions.
 
.EXAMPLE
$credential = Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey
 
Retrieves creation options from Microsoft Entra ID and creates a new passkey on the local authenticator without registering it.
 
.EXAMPLE
Get-EntraPasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey | Register-EntraPasskey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano'
 
Performs end-to-end passkey registration in Microsoft Entra ID in a single pipeline.
 
.EXAMPLE
Get-OktaPasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 | New-Passkey | Register-OktaPasskey
 
Performs end-to-end passkey registration in Okta in a single pipeline.
 
.LINK
Get-PasskeyCreationOptions
 
.LINK
Get-EntraPasskeyRegistrationOptions
 
.LINK
Get-OktaPasskeyRegistrationOptions
 
.LINK
Register-EntraPasskey
 
.LINK
Register-OktaPasskey
 
.LINK
Test-Passkey
 
#>

function New-Passkey
{
    [OutputType([DSInternals.Win32.WebAuthn.AttestationPublicKeyCredential], ParameterSetName = 'Default')]
    [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse], ParameterSetName = 'Okta')]
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Default')]
        [DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions]
        $Options,

        [Parameter(Mandatory = $false, ParameterSetName = 'Default')]
        [ValidateNotNullOrEmpty()]
        [string]
        $HostName,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Okta')]
        [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions]
        $OktaOptions
    )

    process {
        try {
            # Call the WebAuthn API to create a new credential, which will trigger the Windows passkey UI.
            # The `hostName` argument derives the WebAuthn origin and stands in for the rpId when the
            # server-issued options omit it. For Okta, the tenant host is carried on the options object;
            # for the Default path, the caller can supply it via -HostName.
            [DSInternals.Win32.WebAuthn.WebAuthnApi] $api = [DSInternals.Win32.WebAuthn.WebAuthnApi]::new()
            [DSInternals.Win32.WebAuthn.AttestationPublicKeyCredential] $credential =
                if ($PSCmdlet.ParameterSetName -eq 'Okta') {
                    $api.AuthenticatorMakeCredential($OktaOptions.PublicKeyOptions, $OktaOptions.Tenant)
                } else {
                    $api.AuthenticatorMakeCredential($Options, $HostName)
                }

            if ($PSCmdlet.ParameterSetName -eq 'Okta') {
                # Wrap the credential into an Okta-specific attestation response object
                return [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse]::new(
                    $credential, $OktaOptions.PublicKeyOptions.User.Id, $OktaOptions.FactorId)
            }
            else {
                return $credential
            }
        }
        catch {
            $targetObject = if ($PSCmdlet.ParameterSetName -eq 'Okta') { $OktaOptions } else { $Options }
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    $PSItem.Exception,
                    $PSItem.Exception.Message,
                    [System.Management.Automation.ErrorCategory]::InvalidArgument,
                    $targetObject))
        }
    }
}

<#
.SYNOPSIS
Builds a PublicKeyCredentialCreationOptions object for use with New-Passkey.
 
.DESCRIPTION
Constructs a WebAuthn PublicKeyCredentialCreationOptions structure locally, without contacting any relying party. The resulting object can be piped directly to New-Passkey to drive the local authenticator and produce a new credential.
 
This is useful for testing scenarios, custom relying parties, or when interacting with services that don't expose a registration-options endpoint.
 
.PARAMETER RelyingPartyId
The relying party identifier (e.g., 'login.microsoft.com').
 
.PARAMETER RelyingPartyName
The human-readable name of the relying party (e.g., 'Contoso').
 
.PARAMETER UserName
The user account name, typically an email address or username (e.g., 'john@contoso.com').
 
.PARAMETER UserDisplayName
The friendly name of the user (e.g., 'John Doe'). If not provided, the UserName is used.
 
.PARAMETER UserId
The user handle, an opaque identifier for the user. Accepts either a byte array or a Base64Url encoded string. If not provided, a random 32-byte identifier is generated.
 
.PARAMETER Challenge
The challenge bytes to be signed. Accepts either a byte array or a Base64Url encoded string. If not provided, a random challenge will be generated.
 
.PARAMETER Algorithm
The list of COSE algorithms acceptable to the relying party, in order of preference. Defaults to ES256 and RS256.
 
.PARAMETER UserVerification
Specifies the user verification requirement.
 
.PARAMETER AuthenticatorAttachment
Specifies the authenticator attachment type.
 
.PARAMETER ResidentKey
Specifies the resident key (discoverable credential) requirement. Defaults to Required for passkeys.
 
.PARAMETER Attestation
Specifies the attestation conveyance preference.
 
.PARAMETER Timeout
The timeout for the credential creation operation.
 
.PARAMETER Hint
An optional hint to the client about which authenticator type to use (e.g., SecurityKey, ClientDevice, Hybrid).
 
.EXAMPLE
Get-PasskeyCreationOptions -RelyingPartyId 'example.com' -RelyingPartyName 'Example' -UserName 'john@example.com' -UserDisplayName 'John Doe'
 
Builds creation options for a new passkey using mostly default settings.
 
.EXAMPLE
Get-PasskeyCreationOptions -RelyingPartyId 'example.com' -RelyingPartyName 'Example' -UserName 'john@example.com' | New-Passkey
 
Builds creation options and immediately creates a new passkey on the local authenticator.
 
.EXAMPLE
Get-PasskeyCreationOptions -RelyingPartyId 'example.com' -RelyingPartyName 'Example' -UserName 'john@example.com' -Hint SecurityKey -ResidentKey Required -UserVerification Required
 
Builds creation options targeting a security key with required user verification and a discoverable credential.
 
.LINK
New-Passkey
 
.LINK
New-PasskeyRandomChallenge
 
.LINK
Test-Passkey
 
#>

function Get-PasskeyCreationOptions
{
    [OutputType([DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('RelyingParty')]
        [Alias('RpId')]
        [string] $RelyingPartyId,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('RpName')]
        [string] $RelyingPartyName,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $UserName,

        [Parameter(Mandatory = $false)]
        [string] $UserDisplayName,

        [Parameter(Mandatory = $false)]
        [Alias('User', 'UserHandle')]
        [object] $UserId,

        [Parameter(Mandatory = $false)]
        [object] $Challenge = (New-PasskeyRandomChallenge -Length 32),

        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [Alias('Algorithms', 'PubKeyCredParams')]
        [DSInternals.Win32.WebAuthn.COSE.Algorithm[]] $Algorithm = @([DSInternals.Win32.WebAuthn.COSE.Algorithm]::ES256, [DSInternals.Win32.WebAuthn.COSE.Algorithm]::RS256),

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.UserVerificationRequirement] $UserVerification = [DSInternals.Win32.WebAuthn.UserVerificationRequirement]::Preferred,

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.AuthenticatorAttachment] $AuthenticatorAttachment = [DSInternals.Win32.WebAuthn.AuthenticatorAttachment]::Any,

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.ResidentKeyRequirement] $ResidentKey = [DSInternals.Win32.WebAuthn.ResidentKeyRequirement]::Required,

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.AttestationConveyancePreference] $Attestation = [DSInternals.Win32.WebAuthn.AttestationConveyancePreference]::None,

        [Parameter(Mandatory = $false)]
        [ValidateScript({
            if ($PSItem -is [TimeSpan]) {
                [timespan] $min = New-TimeSpan -Seconds 1
                [timespan] $max = New-TimeSpan -Minutes 10
                return $PSItem -ge $min -and $PSItem -le $max
            }
            else {
                throw "Parameter must be a TimeSpan object."
            }
        })]
        [timespan] $Timeout = (New-TimeSpan -Minutes 2),

        [Parameter(Mandatory = $false)]
        [Alias("AuthenticatorType", "CredentialHint", "PublicKeyCredentialHint")]
        [DSInternals.Win32.WebAuthn.PublicKeyCredentialHint] $Hint = [DSInternals.Win32.WebAuthn.PublicKeyCredentialHint]::None
    )

    try {
        # Convert Challenge parameter (accepts byte[] or Base64Url string)
        [byte[]] $challengeBytes = ConvertFrom-Base64UrlParameter -InputObject $Challenge

        # Convert UserId parameter (accepts byte[] or Base64Url string); generate a random user handle if not provided
        [byte[]] $userIdBytes = ConvertFrom-Base64UrlParameter -InputObject $UserId
        if ($null -eq $userIdBytes -or $userIdBytes.Length -eq 0) {
            $userIdBytes = New-PasskeyRandomChallenge -Length 32
        }

        # Fall back to UserName when no UserDisplayName is provided
        if ([string]::IsNullOrEmpty($UserDisplayName)) {
            $UserDisplayName = $UserName
        }

        [uint32] $timeoutMilliseconds = $Timeout.TotalMilliseconds

        # Build the public key credential parameters list
        [System.Collections.Generic.List[DSInternals.Win32.WebAuthn.PublicKeyCredentialParameter]] $pubKeyCredParams =
            [System.Collections.Generic.List[DSInternals.Win32.WebAuthn.PublicKeyCredentialParameter]]::new()
        foreach ($alg in $Algorithm) {
            $pubKeyCredParams.Add([DSInternals.Win32.WebAuthn.PublicKeyCredentialParameter]::new(
                $alg,
                [DSInternals.Win32.WebAuthn.Interop.ApiConstants]::PublicKeyCredentialType))
        }

        [DSInternals.Win32.WebAuthn.RelyingPartyInformation] $relyingParty = [DSInternals.Win32.WebAuthn.RelyingPartyInformation]@{
            Id = $RelyingPartyId
            Name = $RelyingPartyName
        }

        [DSInternals.Win32.WebAuthn.UserInformation] $user = [DSInternals.Win32.WebAuthn.UserInformation]@{
            Id = $userIdBytes
            Name = $UserName
            DisplayName = $UserDisplayName
        }

        [DSInternals.Win32.WebAuthn.AuthenticatorSelectionCriteria] $authenticatorSelection = [DSInternals.Win32.WebAuthn.AuthenticatorSelectionCriteria]@{
            AuthenticatorAttachment = $AuthenticatorAttachment
            UserVerificationRequirement = $UserVerification
            ResidentKey = $ResidentKey
            RequireResidentKey = ($ResidentKey -eq [DSInternals.Win32.WebAuthn.ResidentKeyRequirement]::Required)
        }

        # Convert the credential hint to its DOMString form, if any
        [string] $credentialHint = [DSInternals.Win32.WebAuthn.PublicKeyCredentialHintExtensions]::ToJsonString($Hint)
        [string[]] $credentialHints = if (-not [string]::IsNullOrEmpty($credentialHint)) { @($credentialHint) } else { $null }

        [DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions] $options = [DSInternals.Win32.WebAuthn.PublicKeyCredentialCreationOptions]@{
            RelyingParty = $relyingParty
            User = $user
            Challenge = $challengeBytes
            PublicKeyCredentialParameters = $pubKeyCredParams
            TimeoutMilliseconds = $timeoutMilliseconds
            AuthenticatorSelection = $authenticatorSelection
            Attestation = $Attestation
            Hints = $credentialHints
        }

        return $options
    }
    catch {
        [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::InvalidArgument,
            $RelyingPartyId
        )

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

<#
.SYNOPSIS
Tests a passkey by performing an authentication assertion.
 
.DESCRIPTION
Performs a WebAuthn authentication assertion to test a passkey credential. This triggers the authenticator to sign a challenge,
verifying that the passkey is working correctly.
 
.PARAMETER RelyingPartyId
The relying party identifier (e.g., 'login.microsoft.com').
 
.PARAMETER Challenge
The challenge bytes to be signed. Accepts either a byte array or a Base64Url encoded string.
If not provided, a random challenge will be generated.
 
.PARAMETER UserVerification
Specifies the user verification requirement.
 
.PARAMETER AuthenticatorAttachment
Specifies the authenticator attachment type.
 
.PARAMETER Timeout
The timeout for the operation.
 
.PARAMETER CredentialId
An optional credential ID to test a specific credential. Accepts either a byte array or a Base64Url encoded string.
 
.PARAMETER Hint
An optional hint to the client about which credential source to use (e.g., SecurityKey, ClientDevice, Hybrid).
 
.EXAMPLE
Test-Passkey -RelyingPartyId 'login.microsoft.com'
 
Tests any passkey registered for login.microsoft.com with a random challenge.
 
.EXAMPLE
$challenge = Get-PasskeyRandomChallenge -Length 32
Test-Passkey -RelyingPartyId 'login.microsoft.com' -Challenge $challenge
 
Tests any passkey registered for login.microsoft.com with a specific challenge.
 
.EXAMPLE
$credential = Get-PasskeyWindowsHello | Select-Object -First 1
Test-Passkey -RelyingPartyId $credential.RelyingPartyInformation.Id -CredentialId $credential.CredentialId
 
Tests a specific platform credential.
 
.EXAMPLE
Test-Passkey -RelyingPartyId 'login.microsoft.com' -Hint SecurityKey
 
Tests a passkey with a hint that a security key should be used.
 
.LINK
Get-PasskeyWindowsHello
 
.LINK
New-PasskeyRandomChallenge
 
#>

function Test-Passkey
{
    [OutputType([DSInternals.Win32.WebAuthn.AssertionPublicKeyCredential])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('RelyingParty')]
        [Alias('RpId')]
        [string] $RelyingPartyId,

        [Parameter(Mandatory = $false)]
        [object] $Challenge = (New-PasskeyRandomChallenge -Length 32),

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.UserVerificationRequirement] $UserVerification = [DSInternals.Win32.WebAuthn.UserVerificationRequirement]::Preferred,

        [Parameter(Mandatory = $false)]
        [DSInternals.Win32.WebAuthn.AuthenticatorAttachment] $AuthenticatorAttachment = [DSInternals.Win32.WebAuthn.AuthenticatorAttachment]::Any,

        [Parameter(Mandatory = $false)]
        [timespan] $Timeout = (New-TimeSpan -Minutes 2),

        [Parameter(Mandatory = $false)]
        [object] $CredentialId,

        [Parameter(Mandatory = $false)]
        [Alias("AuthenticatorType", "CredentialHint", "PublicKeyCredentialHint")]
        [DSInternals.Win32.WebAuthn.PublicKeyCredentialHint] $Hint = [DSInternals.Win32.WebAuthn.PublicKeyCredentialHint]::None
    )

    try {
        # Convert Challenge parameter (accepts byte[] or Base64Url string)
        [byte[]] $challengeBytes = ConvertFrom-Base64UrlParameter -InputObject $Challenge

        # Convert CredentialId parameter (accepts byte[] or Base64Url string)
        [byte[]] $credentialIdBytes = ConvertFrom-Base64UrlParameter -InputObject $CredentialId

        # Build the AllowCredentials list if a specific CredentialId was provided
        [DSInternals.Win32.WebAuthn.PublicKeyCredentialDescriptor[]] $allowCredentials = @()

        if ($null -ne $credentialIdBytes -and $credentialIdBytes.Length -gt 0) {
            $allowCredentials += [DSInternals.Win32.WebAuthn.PublicKeyCredentialDescriptor]::new($credentialIdBytes)
        }

        # Convert TimeSpan to milliseconds, while capping to [1, 10 minutes]
        [int] $timeoutMilliseconds = [Math]::Max(1, [Math]::Min(10 * 60 * 1000, $Timeout.TotalMilliseconds))

        [DSInternals.Win32.WebAuthn.WebAuthnApi] $api = [DSInternals.Win32.WebAuthn.WebAuthnApi]::new()

        [string] $credentialHint = [DSInternals.Win32.WebAuthn.PublicKeyCredentialHintExtensions]::ToJsonString($Hint)
        [string[]] $credentialHints = if (![string]::IsNullOrEmpty($credentialHint)) { @($credentialHint) } else { $null }

        [DSInternals.Win32.WebAuthn.AssertionPublicKeyCredential] $response = $api.AuthenticatorGetAssertion(
            $RelyingPartyId,
            $challengeBytes,
            $UserVerification,
            $AuthenticatorAttachment,
            $timeoutMilliseconds,
            $allowCredentials,
            $null,  # extensions
            $false, # browserInPrivateMode
            $null,  # linkedDevice
            $false, # autoFill
            $credentialHints,
            $null,  # authenticatorId
            $null,  # publicKeyCredentialRequestOptionsJson
            [DSInternals.Win32.WebAuthn.WindowHandle]::ForegroundWindow
        )

        return $response
    }
    catch {
        [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $RelyingPartyId
        )

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

<#
.SYNOPSIS
Gets the list of registered authenticator plugins from the Windows registry.
 
.DESCRIPTION
Retrieves information about third-party passkey providers (such as 1Password, Bitwarden, etc.) that are registered
as authenticator plugins in Windows. These plugins are registered under HKLM\SOFTWARE\Microsoft\FIDO.
 
.EXAMPLE
Get-PasskeyAuthenticatorPlugin
 
Lists all registered authenticator plugins.
 
.EXAMPLE
Get-PasskeyAuthenticatorPlugin | Where-Object Enabled -eq $true
 
Lists only enabled authenticator plugins.
 
.EXAMPLE
Get-PasskeyAuthenticatorPlugin | Select-Object -Property Name,PackageFamilyName,Enabled
 
Lists authenticator plugins with selected properties.
 
.LINK
Get-PasskeyAuthenticator
 
#>

function Get-PasskeyAuthenticatorPlugin
{
    [OutputType([DSInternals.Win32.WebAuthn.AuthenticatorPluginInformation])]
    [CmdletBinding()]
    param()

    try {
        [DSInternals.Win32.WebAuthn.AuthenticatorPluginInformation[]] $plugins = [DSInternals.Win32.WebAuthn.WebAuthnApi]::GetPluginAuthenticators()

        if ($null -eq $plugins -or $plugins.Count -eq 0) {
            Write-Verbose 'No authenticator plugins found.'
            return
        }

        foreach ($plugin in $plugins) {
            Write-Output $plugin
        }
    }
    catch {
        [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::ReadError,
            $null
        )

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

<#
.SYNOPSIS
Gets the list of available authenticators from the WebAuthn API.
 
.DESCRIPTION
Retrieves a list of authenticators available on the system using the Windows WebAuthn API.
This includes information about authenticator IDs, names, logos, and lock status.
 
.EXAMPLE
Get-PasskeyAuthenticator
 
Lists all available authenticators.
 
.EXAMPLE
Get-PasskeyAuthenticator | Where-Object { -not $PSItem.Locked }
 
Lists only unlocked authenticators.
 
.EXAMPLE
Get-PasskeyAuthenticator | Format-Table -Prperty AuthenticatorName,Locked
 
Lists authenticator names and lock status in a table.
 
.LINK
Get-PasskeyAuthenticatorPlugin
 
.LINK
Get-PasskeyWindowsHello
 
#>

function Get-PasskeyAuthenticator
{
    [OutputType([DSInternals.Win32.WebAuthn.AuthenticatorDetails])]
    [CmdletBinding()]
    param()

    try {
        [DSInternals.Win32.WebAuthn.AuthenticatorDetails[]] $authenticators = [DSInternals.Win32.WebAuthn.WebAuthnApi]::GetAuthenticatorList()

        if ($null -eq $authenticators -or $authenticators.Count -eq 0) {
            Write-Verbose 'No authenticators found.'
            return
        }

        foreach ($authenticator in $authenticators) {
            Write-Output $authenticator
        }
    }
    catch {
        [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::ReadError,
            $null
        )

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

<#
.SYNOPSIS
Gets the list of platform credentials (passkeys) stored on the system.
 
.DESCRIPTION
Retrieves the list of credentials stored on platform authenticators (such as Windows Hello).
This includes information about credential IDs, relying party information, user information,
and whether credentials are removable or backed up.
 
.PARAMETER RelyingPartyId
Optional relying party ID to filter credentials. If not specified, all credentials are returned.
 
.EXAMPLE
Get-PasskeyWindowsHello
 
Lists all platform credentials.
 
.EXAMPLE
Get-PasskeyWindowsHello -RelyingPartyId 'login.microsoft.com'
 
Lists credentials for a specific relying party.
 
.LINK
Remove-PasskeyWindowsHello
 
.LINK
Test-Passkey
 
.LINK
Get-PasskeyAuthenticator
 
#>

function Get-PasskeyWindowsHello
{
    [OutputType([DSInternals.Win32.WebAuthn.CredentialDetails])]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [Alias('RpId')]
        [string] $RelyingPartyId
    )

    try {
        [DSInternals.Win32.WebAuthn.CredentialDetails[]] $credentials = [DSInternals.Win32.WebAuthn.WebAuthnApi]::GetPlatformCredentialList($RelyingPartyId)

        if ($null -eq $credentials -or $credentials.Count -eq 0) {
            Write-Verbose 'No platform credentials found.'
            return
        }

        foreach ($credential in $credentials) {
            Write-Output $credential
        }
    }
    catch {
        [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::ReadError,
            $RelyingPartyId
        )

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

<#
.SYNOPSIS
Removes a platform credential (passkey) from the system.
 
.DESCRIPTION
Removes a Public Key Credential stored on a platform authenticator (such as Windows Hello).
This operation is irreversible - once deleted, the credential cannot be recovered.
 
.PARAMETER CredentialId
The ID of the credential to be removed. This can be obtained from Get-PasskeyWindowsHello.
Accepts either a byte array or a Base64Url encoded string.
 
.PARAMETER Credential
A CredentialDetails object obtained from Get-PasskeyWindowsHello.
 
.EXAMPLE
$cred = Get-PasskeyWindowsHello | Select-Object -First 1
Remove-PasskeyWindowsHello -CredentialId $cred.CredentialId
 
Removes a specific platform credential by ID.
 
.EXAMPLE
Get-PasskeyWindowsHello | Where-Object { $PSItem.RelyingPartyInformation.Id -eq 'example.com' } | Remove-PasskeyWindowsHello
 
Removes all credentials for a specific relying party using pipeline input.
 
.EXAMPLE
Remove-PasskeyWindowsHello -CredentialId 'dGVzdC1jcmVkZW50aWFsLWlk'
 
Removes a credential using a Base64Url encoded credential ID.
 
.NOTES
Requires Windows with WebAuthn API version 4 or later (Windows 10 2004+).
This operation requires appropriate permissions and may trigger a Windows Security prompt.
 
.LINK
Get-PasskeyWindowsHello
 
#>

function Remove-PasskeyWindowsHello
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ByCredentialId')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'ByCredentialId', Position = 0)]
        [ValidateNotNull()]
        [object] $CredentialId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByCredential', ValueFromPipeline = $true)]
        [ValidateNotNull()]
        [DSInternals.Win32.WebAuthn.CredentialDetails] $InputObject
    )
    try {
        # Get the credential ID from the appropriate parameter
        [byte[]] $credId = $null

        if ($PSCmdlet.ParameterSetName -eq 'ByCredential') {
            $credId = $InputObject.CredentialId
        }
        else {
            # Convert CredentialId from string (Base64Url) to byte[] if necessary
            $credId = ConvertFrom-Base64UrlParameter -InputObject $CredentialId
        }

        # Confirm the operation
        [string] $credentialIdString = [System.Buffers.Text.Base64Url]::EncodeToString($credId)
        if ($PSCmdlet.ShouldProcess($credentialIdString, 'Remove platform credential')) {
            [DSInternals.Win32.WebAuthn.WebAuthnApi]::DeletePlatformCredential($credId)
            Write-Verbose "Successfully removed credential $credentialIdString"
        }
    }
    catch {
        Write-Error -ErrorRecord ([System.Management.Automation.ErrorRecord]::new(
            $PSItem.Exception,
            $PSItem.Exception.Message,
            [System.Management.Automation.ErrorCategory]::WriteError,
            $credId
        ))
    }
}

<#
.SYNOPSIS
Generates a random challenge to be used by WebAuthn.
 
.DESCRIPTION
Returns a cryptographically random byte array of the requested length, suitable for use as a WebAuthn challenge during credential creation or assertion.
 
.PARAMETER Length
The length of the challenge in bytes.
 
.EXAMPLE
New-PasskeyRandomChallenge -Length 64
 
Generates a random 64-byte challenge.
 
.EXAMPLE
$challenge = New-PasskeyRandomChallenge
Test-Passkey -RelyingPartyId 'login.microsoft.com' -Challenge $challenge
 
Generates a default 32-byte challenge and uses it for a passkey assertion.
 
.LINK
New-Passkey
 
.LINK
Test-Passkey
 
.LINK
Get-PasskeyCreationOptions
 
#>

function New-PasskeyRandomChallenge
{
    [CmdletBinding()]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory = $false)]
        [ValidateRange(16, 64)]
        [int] $Length = 32
    )

    [byte[]] $challenge = [byte[]]::new($Length)
    [System.Security.Cryptography.RandomNumberGenerator] $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try {
        $rng.GetBytes($challenge)
        return $challenge
    }
    finally {
        $rng.Dispose()
    }
}

<#
.SYNOPSIS
Converts a Base64Url encoded string or byte array to a byte array.
 
.PARAMETER InputObject
The input object to convert. Can be a Base64Url encoded string or a byte array.
 
.NOTES
This is a helper function used internally for parameter conversion.
#>

function ConvertFrom-Base64UrlParameter
{
    [CmdletBinding()]
    [OutputType([byte[]])]
    param(
        [Parameter(Mandatory = $false, Position = 0)]
        [object] $InputObject
    )

    if ($null -eq $InputObject) {
        return $null
    } elseif ($InputObject -is [string]) {
        # Convert from Base64Url string to byte array
        return [DSInternals.Win32.WebAuthn.Base64UrlConverter]::FromBase64UrlString($InputObject)
    } elseif ($null -ne ($InputObject -as [byte[]])) {
        # Nothing to convert
        return $InputObject
    } else {
        throw [System.ArgumentException]::new("The value must be a byte array or a Base64Url encoded string.")
    }
}

# Functions and aliases are filtered by FunctionsToExport / AliasesToExport in the manifest.