DSInternals.Passkeys.psm1
|
# Needed for [Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod] type Import-Module -Name Microsoft.Graph.Identity.SignIns -ErrorAction Stop # Variables used for Okta connection lifecycle management New-Variable -Name OktaToken -Value $null -Scope Script New-Variable -Name OktaRevocationInfo -Value $null -Scope Script function Get-EntraIDPasskeyRegistrationOptions { [OutputType([DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnCredentialCreationOptions])] param ( [Parameter(Mandatory = $true)] [ValidateScript({ return $_ -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" -or $true -eq [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $false)] [ValidateScript({ if ($_ -is [TimeSpan]) { $min = New-TimeSpan -Minutes 5 $max = New-TimeSpan -Minutes 43200 return $_ -ge $min -and $_ -le $max } else { throw "Parameter must be a TimeSpan object." } })] [Alias('Timeout')] [TimeSpan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) try { Write-Debug "UserId ${UserId} TokenLifetimeSeconds ${ChallengeTimeout}" # Generate the user-specific URL, e.g., https://graph.microsoft.com/beta/users/af4cf208-16e0-429d-b574-2a09c5f30dea/authentication/fido2Methods/creationOptions [string] $credentialOptionsUrl = '/beta/users/{0}/authentication/fido2Methods/creationOptions' -f [uri]::EscapeDataString($UserId) Write-Debug ('Credential options url: ' + $credentialOptionsUrl) [string] $response = Invoke-MgGraphRequest -Method GET ` -Uri $credentialOptionsUrl ` -Body @{ challengeTimeoutInMinutes = $ChallengeTimeout.TotalMinutes } ` -OutputType Json Write-Debug ('Credential options response: ' + $response) # Parse JSON response return [DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnCredentialCreationOptions]::Create($response) } catch { throw $_ } } function Invoke-OktaWebRequest { param( $Path, $Query, $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post, $Body, $ContentType = "application/json" ) Write-Debug "Path ${Path}" Write-Debug "Query ${Query}" Write-Debug "Method ${Method}" Write-Debug ('Body ' + ($Body | ConvertTo-Json)) Write-Debug "Content type ${ContentType}" $tokenType = $Script:OktaToken.TokenType $token = $Script:OktaToken.AccessToken $headers = @{ "Accept" = "application/json" "Authorization" = "${tokenType} ${token}" } $tenant = ([System.UriBuilder]($Script:OktaToken.AuthenticationResultMetadata.TokenEndpoint)).Host Write-Debug "Tenant ${Tenant}" $uriBuilder = New-Object System.UriBuilder $uriBuilder.Scheme = "https" $uriBuilder.Host = $tenant $uriBuilder.Path = $Path $uriBuilder.Query = $Query $uri = $uriBuilder.ToString() Write-Debug "Uri ${uri}" return Invoke-WebRequest -Uri $uri ` -Method $Method ` -Headers $headers ` -ContentType $ContentType ` -Body $Body } function Get-OktaPasskeyRegistrationOptions { [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions])] param( [Parameter(Mandatory = $true)] [ValidatePattern("^[A-Za-z0-9_-]{20}$")] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $false)] [ValidateScript({ if ($_ -is [TimeSpan]) { $min = New-TimeSpan -Seconds 1 $max = New-TimeSpan -Seconds 86400 return $_ -ge $min -and $_ -le $max } else { throw "Parameter must be a TimeSpan object." } })] [Alias('Timeout')] [TimeSpan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) begin { if ($null -eq $Script:OktaToken) { throw 'Not connected to Okta, call Connnect-Okta to get started.' } } process { try { Write-Debug "In Get-OktaPasskeyRegistrationOptions with ${UserId} and ${ChallengeTimeout}" $TokenLifetimeSeconds = $ChallengeTimeout.TotalSeconds Write-Debug "TokenLifetimeSeconds ${TokenLifetimeSeconds}" [string] $credentialOptionsPath = "/api/v1/users/${UserId}/factors" [string] $credentialOptionsQuery = "tokenLifetimeSeconds=${TokenLifetimeSeconds}&activate=true" Write-Debug ('Credential options path: ' + $credentialOptionsPath) Write-Debug ('Credential options query: ' + $credentialOptionsQuery) $body = @{ factorType = "webauthn" provider = "FIDO" } | ConvertTo-Json -Compress Write-Debug ('Credential options payload: ' + $body) [string] $response = Invoke-OktaWebRequest -Path $credentialOptionsPath ` -Query $credentialOptionsQuery ` -Body $body Write-Debug ('Credential options response: ' + $response) # Parse JSON response $options = [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions]::Create($response) # Okta appears to omit relying party id in the options, but it is required for credential creation # So set default to the tenant we are talking to, which is probably what the user wants anyway if ($null -eq $options.Embedded.PublicKeyOptions.RelyingParty.Id) { Write-Debug ('Setting relying party id to ' + ([System.UriBuilder]($Script:OktaToken.AuthenticationResultMetadata.TokenEndpoint)).Host) $options.Embedded.PublicKeyOptions.RelyingParty.Id = ([System.UriBuilder]($Script:OktaToken.AuthenticationResultMetadata.TokenEndpoint)).Host } Write-Debug ('Credential options: ' + ($options | Out-String)) return $options } catch { throw $_ } } } <# .SYNOPSIS Retrieves creation options required to generate and register a Microsoft Entra ID or Okta compatible passkey. .PARAMETER UserId The unique identifier of user. For Entra ID, this is the object id (guid) or UPN. For Okta, this is the unique identifier of Okta user. .PARAMETER ChallengeTimeout Overrides the timeout of the server-generated challenge returned in the request. For Entra ID, the default value is 5 minutes, with the accepted range being between 5 minutes and 30 days. For Okta, the default value is 300 second, with the accepted range being between 1 second and 1 day. .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Get-PasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Get-PasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' -ChallengeTimeout (New-TimeSpan -Minutes 10) .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 PS \> Get-PasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 PS \> Get-PasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 -ChallengeTimeout (New-TimeSpan -Seconds 60) .NOTES Self-service operations aren't supported for Entra ID. More info about Entra ID at https://learn.microsoft.com/en-us/graph/api/fido2authenticationmethod-creationoptions More info about Okta at https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/enrollFactor #> function Get-PasskeyRegistrationOptions { [OutputType([DSInternals.Win32.WebAuthn.WebauthnCredentialCreationOptions])] param( [Parameter(Mandatory = $true)] [Alias('User')] [ValidateScript({ if ($_ -match "^[A-Za-z0-9_-]{20}$" -or $true -eq [guid]::TryParse($_, $([ref][guid]::Empty)) -or $_ -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") {return $true} return $false })] [string] $UserId, [Parameter(Mandatory = $false)] [Alias('Timeout')] [timespan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) begin { Write-Debug "In Get-PasskeyRegistrationOptions with ${UserId} and ${ChallengeTimeout}" $IsEntraID = ($UserId -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" -or $true -eq [guid]::TryParse($UserId, $([ref][guid]::Empty))) Write-Debug "IsEntraID: ${IsEntraId}" if ($IsEntraID) { $min = New-TimeSpan -Minutes 5 $max = New-TimeSpan -Minutes 43200 if ($ChallengeTimeout -gt $max -or $ChallengeTimeout -lt $min) { Write-Error "Cannot validate argument on parameter 'ChallengeTimeout' which must be a valid TimeSpan between 5 and 43200 minutes for $_." -ErrorAction Stop } } else { $min = New-TimeSpan -Seconds 1 $max = New-TimeSpan -Seconds 86400 if ($ChallengeTimeout -gt $max -or $ChallengeTimeout -lt $min) { Write-Error "Cannot validate argument on parameter 'ChallengeTimeout' which must be a valid TimeSpan between 1 and 86400 seconds for $_." -ErrorAction Stop } if ($UserId -notmatch "^[A-Za-z0-9_-]{20}$") { Write-Error "Cannot validate argument on parameter 'UserID' which must the unique idenitier for the user for Okta." -ErrorAction Stop } if ($null -eq $Script:OktaToken) { throw 'Not connected to Okta, call Connnect-Okta to get started.' } } } process { $Options = $null try { if ($IsEntraID) { Write-Debug "Calling Get-EntraIDPasskeyRegistrationOptions with ${UserId} and ${ChallengeTimeout}" $Options = Get-EntraIDPasskeyRegistrationOptions -UserId $UserId -ChallengeTimeout $ChallengeTimeout } else { Write-Debug "Calling Get-OktaPasskeyRegistrationOptions with ${UserId} and ${ChallengeTimeout}" $Options = Get-OktaPasskeyRegistrationOptions -UserId $UserId -ChallengeTimeout $ChallengeTimeout } return $Options } catch { $errorRecord = New-Object Management.Automation.ErrorRecord( $_.Exception, $_.Exception.Message, [Management.Automation.ErrorCategory]::InvalidArgument, $Options ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } } function Register-EntraIDPasskey { [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod])] param( [Parameter(Mandatory = $true)] [ValidateScript({ return $_ -match "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" -or $true -eq [guid]::TryParse($_, $([ref][guid]::Empty)) })] [Alias('User')] [string] $UserId, [ValidateScript({ if ([string]::IsNullOrEmpty($_.DisplayName)) { throw "Passkey 'DisplayName' field may not be null or empty." } return $true })] [DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnAttestationResponse] $Passkey ) try { # Generate the user-specific URL, e.g., https://graph.microsoft.com/beta/users/af4cf208-16e0-429d-b574-2a09c5f30dea/authentication/fido2Methods [string] $registrationUrl = '/beta/users/{0}/authentication/fido2Methods' -f [uri]::EscapeDataString($UserId) Write-Debug ('Registration URL: ' + $registrationUrl) [string] $response = Invoke-MgGraphRequest ` -Method POST ` -Uri $registrationUrl ` -OutputType Json ` -ContentType 'application/json' ` -Body $Passkey.ToString() Write-Debug ('Registration response: ' + $response) return [Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod]::FromJsonString($response) } catch { throw $_ } } function Register-OktaPasskey { [CmdletBinding(DefaultParameterSetName = 'New')] [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaFido2AuthenticationMethod])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Existing')] [Parameter(Mandatory = $true, ParameterSetName = 'New')] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $true, ParameterSetName = 'Existing', ValueFromPipeline = $true)] [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse] $Passkey, [Parameter(Mandatory = $false, ParameterSetName = 'New')] [ValidateScript({ if ($_ -is [TimeSpan]) { $min = New-TimeSpan -Seconds 1 $max = New-TimeSpan -Seconds 86400 return $_ -ge $min -and $_ -le $max } else { throw "Parameter must be a TimeSpan object." } })] [Alias('Timeout')] [timespan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) try { if ($null -eq $Script:OktaToken) { throw 'Not connected to Okta, call Connnect-Okta to get started.' } $userId = $Passkey.UserId $factorId = $Passkey.FactorId [string] $registrationPath = "/api/v1/users/${userId}/factors/${factorId}/lifecycle/activate" Write-Debug ('Registration path: ' + $registrationPath) [string] $response = Invoke-OktaWebRequest -Path $registrationPath ` -Body $Passkey.ToString() Write-Debug ('Registration response: ' + $response) return [DSInternals.Win32.WebAuthn.Okta.OktaFido2AuthenticationMethod]::FromJsonString($response) } catch { throw $_ } } <# .SYNOPSIS Registers a new passkey in Microsoft Entra ID, or Okta. .PARAMETER UserId The unique identifier of user. For Entra ID, this is the object id (guid) or UPN. For Okta, this is the unique identifier of Okta user. .PARAMETER ChallengeTimeout Overrides the timeout of the server-generated challenge returned in the request. For Entra ID, the default value is 5 minutes, with the accepted range being between 5 minutes and 30 days. For Okta, the default value is 300 second, with the accepted range being between 1 second and 1 day. .PARAMETER Passkey The passkey to be registered. .PARAMETER DisplayName Custom name given to the Entra ID registered passkey. .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Register-Passkey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano' .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Register-Passkey -UserId 'AdeleV@contoso.com' -DisplayName 'YubiKey 5 Nano' -ChallengeTimeout (New-TimeSpan -Minutes 10) .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Get-PasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey -DisplayName 'YubiKey 5 Nano' | Register-Passkey -UserId 'AdeleV@contoso.com' .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 PS \> Register-Passkey -UserId 00eDuihq64pgP1gVD0x7 .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 PS \> Get-PasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 | New-Passkey | Register-Passkey .NOTES More info for Entra ID at https://learn.microsoft.com/en-us/graph/api/authentication-post-fido2methods More info for Okta at https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/activateFactor #> function Register-Passkey { [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaFido2AuthenticationMethod], ParameterSetName = 'OktaNew')] [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphFido2AuthenticationMethod], ParameterSetName = 'EntraIDNew')] param( [Parameter(Mandatory = $true, ParameterSetName = 'EntraIDNew')] [Parameter(Mandatory = $true, ParameterSetName = 'Existing')] [Parameter(Mandatory = $true, ParameterSetName = 'OktaNew')] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $true, ParameterSetName = 'Existing', ValueFromPipeline = $true)] [DSInternals.Win32.WebAuthn.WebauthnAttestationResponse] $Passkey, [Parameter(Mandatory = $true, ParameterSetName = 'EntraIDNew')] [string] $DisplayName, [Parameter(Mandatory = $false, ParameterSetName = 'EntraIDNew')] [Parameter(Mandatory = $false, ParameterSetName = 'OktaNew')] [Alias('Timeout')] [timespan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) begin { if ($null -ne $Passkey -and $Passkey.GetType() -eq ([DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnAttestationResponse]) -and [string]::IsNullOrEmpty($Passkey.displayName)) { throw "Parameter 'DisplayName' may not be null or empty." } } process { switch ($PSCmdlet.ParameterSetName) { 'Existing' { switch ($Passkey.GetType()) { ([DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnAttestationResponse]) { return Register-EntraIDPasskey -UserId $UserId -Passkey $Passkey } ([DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse]) { if ($null -eq $Script:OktaToken) { throw 'Not connected to Okta, call Connnect-Okta to get started.' } return Register-OktaPasskey -UserId $UserId -Passkey $Passkey } } } default { [DSInternals.Win32.WebAuthn.WebauthnCredentialCreationOptions] $registrationOptions = Get-PasskeyRegistrationOptions -UserId $UserId -ChallengeTimeout $ChallengeTimeout [DSInternals.Win32.WebAuthn.WebauthnAttestationResponse] $passkey = New-Passkey -Options $registrationOptions -DisplayName $DisplayName # Recursive call with the 'Existing' parameter set return Register-Passkey -UserId $UserId -Passkey $passkey } } } } <# .SYNOPSIS Creates a new Microsoft Entra ID or Okta compatible passkey. .PARAMETER Options Options required to generate a Microsoft Entra ID or Okta compatible passkey. .PARAMETER DisplayName Custom name given to the Entra ID registered passkey. .EXAMPLE PS \> Connect-MgGraph -Scopes 'UserAuthenticationMethod.ReadWrite.All' PS \> Get-PasskeyRegistrationOptions -UserId 'AdeleV@contoso.com' | New-Passkey -DisplayName 'YubiKey 5 Nano' | Register-Passkey -UserId 'AdeleV@contoso.com' .EXAMPLE PS \> New-Passkey -Options $options .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 PS \> Get-PasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 | New-Passkey #> function New-Passkey { [OutputType([DSInternals.Win32.WebAuthn.WebauthnAttestationResponse])] [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [DSInternals.Win32.WebAuthn.WebauthnCredentialCreationOptions] $Options, [Parameter(Mandatory = $false)] [ValidateLength(1, 30)] [string] $DisplayName ) try { [DSInternals.Win32.WebAuthn.WebAuthnApi] $api = [DSInternals.Win32.WebAuthn.WebAuthnApi]::new() [DSInternals.Win32.WebAuthn.PublicKeyCredential] $credential = $api.AuthenticatorMakeCredential($Options.PublicKeyOptions) switch ($Options.GetType()) { ([DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnCredentialCreationOptions]) { if ([string]::IsNullOrEmpty($DisplayName)) { throw "Parameter 'DisplayName' may not be null or empty." } return [DSInternals.Win32.WebAuthn.EntraID.MicrosoftGraphWebauthnAttestationResponse]::new($credential, $DisplayName) } ([DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions]) { return [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse]::new($credential, $Options.PublicKeyOptions.User.Id, $Options.Id) } } } catch { [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_, $_.Message, [System.Management.Automation.ErrorCategory]::InvalidArgument, $Options ) $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 PS \> Test-Passkey -RelyingPartyId 'login.microsoft.com' Tests any passkey registered for login.microsoft.com with a random challenge. .EXAMPLE PS \> $challenge = Get-PasskeyRandomChallenge -Length 32 PS \> Test-Passkey -RelyingPartyId 'login.microsoft.com' -Challenge $challenge Tests any passkey registered for login.microsoft.com with a specific challenge. .EXAMPLE PS \> $credential = Get-PasskeyWindowsHello | Select-Object -First 1 PS \> Test-Passkey -RelyingPartyId $credential.RelyingPartyInformation.Id -CredentialId $credential.CredentialId Tests a specific platform credential. .EXAMPLE PS \> Test-Passkey -RelyingPartyId 'login.microsoft.com' -Hint SecurityKey Tests a passkey with a hint that a security key should be used. #> function Test-Passkey { [OutputType([DSInternals.Win32.WebAuthn.PublicKeyCredential])] [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 ) 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() $response = $api.AuthenticatorGetAssertion( $RelyingPartyId, $challengeBytes, $UserVerification, $AuthenticatorAttachment, $timeoutMilliseconds, $allowCredentials, $null, # extensions [DSInternals.Win32.WebAuthn.CredentialLargeBlobOperation]::None, $null, # largeBlob $false, # browserInPrivateMode $null, # linkedDevice $false, # autoFill $Hint, # credentialHints $null, # remoteWebOrigin $null, # authenticatorId $null, # publicKeyCredentialRequestOptionsJson [DSInternals.Win32.WebAuthn.WindowHandle]::ForegroundWindow ) return $response } catch { [System.Management.Automation.ErrorRecord] $errorRecord = [System.Management.Automation.ErrorRecord]::new( $_.Exception, $_.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 PS \> Get-PasskeyAuthenticatorPlugin Lists all registered authenticator plugins. .EXAMPLE PS \> Get-PasskeyAuthenticatorPlugin | Where-Object Enabled -eq $true Lists only enabled authenticator plugins. .EXAMPLE PS \> Get-PasskeyAuthenticatorPlugin | Select-Object -Property Name,PackageFamilyName,Enabled Lists authenticator plugins with selected properties. #> 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( $_.Exception, $_.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 PS \> Get-PasskeyAuthenticator Lists all available authenticators. .EXAMPLE PS \> Get-PasskeyAuthenticator | Where-Object { -not $PSItem.Locked } Lists only unlocked authenticators. .EXAMPLE PS \> Get-PasskeyAuthenticator | Format-Table -Prperty AuthenticatorName,Locked Lists authenticator names and lock status in a table. #> 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( $_.Exception, $_.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 PS \> Get-PasskeyWindowsHello Lists all platform credentials. .EXAMPLE PS \> Get-PasskeyWindowsHello -RelyingPartyId 'login.microsoft.com' Lists credentials for a specific relying party. #> 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( $_.Exception, $_.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 PS \> $cred = Get-PasskeyWindowsHello | Select-Object -First 1 PS \> Remove-PasskeyWindowsHello -CredentialId $cred.CredentialId Removes a specific platform credential by ID. .EXAMPLE PS \> Get-PasskeyWindowsHello | Where-Object { $_.RelyingPartyInformation.Id -eq 'example.com' } | Remove-PasskeyWindowsHello Removes all credentials for a specific relying party using pipeline input. .EXAMPLE PS \> 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. #> 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( $_.Exception, $_.Exception.Message, [System.Management.Automation.ErrorCategory]::WriteError, $credId )) } } <# .SYNOPSIS Retrieves the Microsoft Graph endpoint URL. .NOTES Dynamic URL retrieval is used to support Azure environments, like Azure Public, Azure Government, or Azure China. #> function Get-MgGraphEndpoint { [CmdletBinding()] [OutputType([string])] param() try { [Microsoft.Graph.PowerShell.Authentication.AuthContext] $context = Get-MgContext -ErrorAction Stop if($null -ne $context) { return (Get-MgEnvironment -Name $context.Environment -ErrorAction Stop).GraphEndpoint } } catch { $errorRecord = New-Object Management.Automation.ErrorRecord( (New-Object Exception('Not connected to Microsoft Graph.')), 'Not connected to Microsoft Graph.', [Management.Automation.ErrorCategory]::ConnectionError, $context ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } <# .SYNOPSIS Retrieves an access token to interact with Okta APIs. .PARAMETER Tenant The unique identifier of Okta tenant, like 'example.okta.com'. .PARAMETER ClientId The client id of the Okta application used to obtain an access token. .PARAMETER Scopes Scopes to request for the access token. Defaults to 'okta.users.manage'. .PARAMETER JsonWebKey The JSON Web Key used to authenticate to the Okta application, in order to obtain access token using the client credentials OAuth flow. .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 .EXAMPLE PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 -Scopes @('okta.users.manage','okta.something.else') .EXAMPLE PS \> $jwk = '{"kty":"RSA","kid":"EE3QB0WvhuOwR9DuR6717OERKbDrBemrDKOK4Xvbf8c","d":"TmljZSB0cnkhICBCdXQgdGhpcyBpc...' PS \> Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 -Scopes @('okta.users.manage','okta.something.else') -JsonWebKey $jwk #> function Connect-Okta { param( [Parameter(Mandatory = $true, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $true, ParameterSetName = 'AuthorizationCode')] [ValidatePattern('^[a-zA-Z0-9-]+\.okta(?:-emea|preview|\.mil)?\.com$')] [string] $Tenant, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'AuthorizationCode')] [ValidatePattern('^[A-Za-z0-9_-]{20}$')] [string] $ClientId, [Parameter(Mandatory = $false, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $false, ParameterSetName = 'AuthorizationCode')] [string[]] $Scopes = @('okta.users.manage'), [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientCredentials')] [Alias('jwk')] [string] $JsonWebKey ) try { $Script:OktaRevocationInfo = [PSCustomObject] @{ ClientId = $ClientId RevocationToken = $null } switch ($PSCmdlet.ParameterSetName){ 'AuthorizationCode' { Write-Debug "No JWK found, assuming public client intended" $publicClientApp = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId). WithExperimentalFeatures(). WithOidcAuthority("https://${tenant}/"). WithRedirectUri("http://localhost:8080/login/callback"). Build() $Script:OktaToken = $publicClientApp.AcquireTokenInteractive($Scopes).ExecuteAsync().GetAwaiter().GetResult() if ($null -ne $Script:OktaToken) { Write-Host 'Okta access token successfully retrieved.' } } 'ClientCredentials' { Write-Debug "JWK found, assuming confidential client intended" $jwk = [Microsoft.IdentityModel.Tokens.JsonWebKey]::new($JsonWebKey) $signingCredentials = [Microsoft.IdentityModel.Tokens.SigningCredentials]::new($jwk,'RS256') $issuer = $ClientId $audience = "https://${tenant}/oauth2/v1/token" $subject = [System.Security.Claims.ClaimsIdentity]::new() $subject.Claims.Add([System.Security.Claims.Claim]::new('sub',$ClientId)) $notBefore = (Get-Date) $expires = (Get-Date).AddMinutes(60) $issuedAt = $notBefore $tokenHandler = [System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler]::new() $securityToken = $tokenHandler.CreateJwtSecurityToken($issuer, $audience, $subject, $notBefore, $expires, $issuedAt, $signingCredentials) $revocationToken = $tokenHandler.CreateJwtSecurityToken($issuer, "https://${tenant}/oauth2/v1/revoke", $subject, $notBefore, $expires, $issuedAt, $signingCredentials) $assertion = $tokenHandler.WriteToken($securityToken) $Script:OktaRevocationInfo.RevocationToken = $tokenHandler.WriteToken($revocationToken) $confidentialClientApp = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientId). WithClientAssertion($assertion). WithOidcAuthority("https://${tenant}"). Build() $Script:OktaToken = $confidentialClientApp.AcquireTokenForClient($Scopes).ExecuteAsync().GetAwaiter().GetResult() if ($null -ne $Script:OktaToken -and $null -ne $Script:OktaRevocationInfo.RevocationToken) { Write-Host 'Okta access and revocation tokens successfully retrieved.' } } } } catch { throw } } <# .SYNOPSIS Revokes Okta access token. .EXAMPLE PS \> Disconnect-Okta .DESCRIPTION Revokes the Okta access token cached from the call to `Connect-Okta`. .LINK https://developer.okta.com/docs/guides/revoke-tokens/main/ #> function Disconnect-Okta { if ($null -ne $Script:OktaToken) { [string] $revocationPath = "/oauth2/v1/revoke" Write-Debug ('Revocation path: ' + $revocationPath) $body = @{ client_id = $Script:OktaRevocationInfo.ClientId token = $Script:OktaToken.AccessToken token_type_hint = "access_token" } if ($null -ne $Script:OktaRevocationInfo.RevocationToken) { $body.Add('client_assertion_type',"urn:ietf:params:oauth:client-assertion-type:jwt-bearer") $body.Add('client_assertion',$Script:OktaRevocationInfo.RevocationToken) } Write-Debug ('Revocation payload: ' + ($body | ConvertTo-Json)) [string] $response = Invoke-OktaWebRequest -Uri $revocationPath ` -ContentType "application/x-www-form-urlencoded" ` -Body $body $Script:OktaToken = $null $Script:OktaRevocationInfo.RevocationToken = $null if ($response.Length -eq 0 -and $null -eq $Script:OktaToken) { Write-Host 'Okta access token successfully revoked.' } } } <# .SYNOPSIS Generates a random challenge to be used by WebAuthn. .PARAMETER Length The length of the challenge in bytes. .EXAMPLE PS \> New-PasskeyRandomChallenge -Length 32 Generates a random 32-byte challenge. #> function New-PasskeyRandomChallenge { [CmdletBinding()] [OutputType([byte[]])] param( [Parameter(Mandatory = $false)] [ValidateRange(16, 64)] [int] $Length = 32 ) [byte[]] $challenge = [byte[]]::new($Length) $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.") } } New-Alias -Name Register-MgUserAuthenticationFido2Method -Value Register-Passkey Export-ModuleMember -Function 'Get-PasskeyRegistrationOptions','New-Passkey','Register-Passkey','Test-Passkey','Connect-Okta','Disconnect-Okta','Invoke-OktaWebRequest','Get-PasskeyAuthenticatorPlugin','Get-PasskeyAuthenticator','Get-PasskeyWindowsHello','Remove-PasskeyWindowsHello', 'New-PasskeyRandomChallenge' ` -Alias 'Register-MgUserAuthenticationFido2Method' # SIG # Begin signature block # MIIvkwYJKoZIhvcNAQcCoIIvhDCCL4ACAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAWVWrhVmQjxoZ3 # bcx/Tiqo9RHpF3I+qii/3zoHVvC98qCCE6UwggWQMIIDeKADAgECAhAFmxtXno4h # MuI5B72nd3VcMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0z # ODAxMTUxMjAwMDBaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/z # G6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZ # anMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7s # Wxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL # 2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfb # BHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3 # JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3c # AORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqx # YxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0 # viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aL # T8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1Ud # EwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzf # Lmc/57qYrhwPTzANBgkqhkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNk # aA9Wz3eucPn9mkqZucl4XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjS # PMFDQK4dUPVS/JA7u5iZaWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK # 7VB6fWIhCoDIc2bRoAVgX+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eB # cg3AFDLvMFkuruBx8lbkapdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp # 5aPNoiBB19GcZNnqJqGLFNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msg # dDDS4Dk0EIUhFQEI6FUy3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vri # RbgjU2wGb2dVf0a1TD9uKFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ7 # 9ARj6e/CVABRoIoqyc54zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5 # nLGbsQAe79APT0JsyQq87kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3 # i0objwG2J5VT6LaJbVu8aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0H # EEcRrYc9B9F1vM/zZn4wggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G # CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C # 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce # 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da # E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T # SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA # FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh # D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM # 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z # 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05 # huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY # mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP # /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN # BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry # sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL # IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf # Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh # OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh # dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV # 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j # wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH # Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC # XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l # /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW # eE4wggdZMIIFQaADAgECAhANqK80cCX+jsAYDGB/BSyeMA0GCSqGSIb3DQEBCwUA # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz # ODQgMjAyMSBDQTEwHhcNMjYwMTA1MDAwMDAwWhcNMjkwMTA0MjM1OTU5WjBhMQsw # CQYDVQQGEwJDWjEOMAwGA1UEBxMFUHJhaGExIDAeBgNVBAoTF01nci4gTWljaGFl # bCBHcmFmbmV0dGVyMSAwHgYDVQQDExdNZ3IuIE1pY2hhZWwgR3JhZm5ldHRlcjCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJrpm0HI6ak/Uj3iqs64nuUT # WZAkxHVeJKwnd3XR0Ge/oE1STGkmydGCZ8VR9jMcIHK+kYteESboRguDVk7BTxEv # SKTEDG0lt1KLr0KUuWI2EUykxtcpR7wOjZ/VBMPeiwPQqccY/ZnwF0H6P43MJSIf # WYVhTLb5R8ueHkjxRGkMPgIcuV4W3TqIEQKvO9NEMC0Tv2nPXPab5u20QdixQaep # 05DEBL1cd9L0NYvbgUi8JQ63I/P7fqrlC6zXZb61wDDTNxwtdFIlR7jvAFqhY1bX # Qfb8bmL4KXH9Sv3hHIDUUfitghKh4RoQWitVTpf+uzLPG0Dr1UH8RbWIYQRXCZhr # 4RJzmt3+i0f+IZSJkRlXBVhn9GeQTk3yaUwLFyz5evTYh5IBaMNA+1BChpswlB32 # PoOjg4eJr3/nArLKN3UZCy9PQ0F+y0J+T/UkGCyv0Ws+zxiZlR2A6ekjGnP5x+8g # n2S3Hf0rMCfCgudgT12S8tXPSdI88TzsihLj8iJT9ljgS0bJSjNykYaBK8BXYYJ3 # PvBn7px9G7b7WOPieyp7rTDkmyWBG5+vVIOFjJUgMIMJChsc5f11iHNcdo7FsVwS # H8MftQ0rUSktj1xK4/p9zwhQpI+eXE0l9YIP08JmVOLtgu2PzXcCT/El+/8+XUw/ # X63cEIgp49URPwtmU0UhAgMBAAGjggIDMIIB/zAfBgNVHSMEGDAWgBRoN+Drtjv4 # XxGG+/5hewiIZfROQjAdBgNVHQ4EFgQUUAbobwgAP24SnIkacVlc4i61qWgwPgYD # VR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdp # Y2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcD # AzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdpY2VydC5jb20v # RGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0Ex # LmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwgZQGCCsG # AQUFBwEBBIGHMIGEMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0 # MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggIBAI/LhB69MYV+zYchS+lShCm1 # H7P/sHkhIctl5ClI6fFpsqc7uAZlH2JjUNYDkkIUvoGk5teyJdLXrycGQIVwH26i # DJm6VDoOOalOjOHh1Om/dP3Cl3XPUM1KRyj39DNnLvPB/5VxIbGoz9yoaTvHAQnu # B5THrdo0nWyPtDgTF2ItJFVky4Uh9cE/ggCifsDUFMwAVKDZ1YvwNspO/ajwdLTM # l121TKX3x5eA8KL4bO3LVvE5GQIqQx3PVqTn4jwFlxjDaBh/RE6yo2UwPwIIfTfj # XTHuziTtFgvVhHKFS55Cxt4h3nCrPEnCfSdG/oNOF8TgLWA873V4T1Qqkdi8aXuA # BWu5GhemjorxFxqeSLrskfTnGO2a5WQ6hmqmz8jU/Ulau8MvwRN8ZSyDJBYc8iuF # VL1+abP12QfM/O8VkqYHbeslkC0qndAbJlT6icTd4CPeBXTwXtGY0hixRxcAkq3y # gKSs7EeGGY6Ytc1pANGDX6PBjVxaf0XfwhkwG35iAA3Ix1muNoB4Nt2HySbkVGDh # ZT5seWy/D2Z44NTAMcGh7WcBGp0RJ/372LGP58ZgSPp/0EtlhaCX1UoQo7Nv6XsY # 1qu0RC+G/lOTl1C9o3eya4bf5y0GZlrNgW3U4nxi4UcRwlpmRaH1hbuz5BQoImaz # mn6pOiSVBvJE8R1OA+9UMYIbRDCCG0ACAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUG # A1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQg # RzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhANqK80cCX+ # jsAYDGB/BSyeMA0GCWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKA # AKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIPhcObz3bAe07u76x/pDjJzC # LuvYk5Lpaw3SnnEyrqoBMA0GCSqGSIb3DQEBAQUABIICAGtwvX1458I02e6itt54 # pfqft8q+pibXG/cLXe59YWFLyEnOvJRfOO0IXSUGZ20p8UcM0pa5kEuzcdJg21qS # Cm26n9eGlsoy6l+FJJd+9ybUYObSY6Do8Gc6U3Pa0hwAw+Z+2DnlSdB7Wg7lpQ3M # VlJDoAh5tvI8boVex+D2lXvKaZGtbBZ1QZ12/NVNLlNhkP9oAN4RcRHxTwi5Rpzv # rcUrm3RXFPZFomXfZtMqATlhRt/eSkDVFyisaQbdEcvkq+GrtQ+OG0AEl51BZQoC # bVoOJFwXqAd65wbxCvEbiHDTKGsjAG2v2vV/5N8+6fIcKLO4QfkWv5qS0lmzmHGv # Ht6hlgZGZB2UVLRm+2jkh0BOcZpDw4eeS5Ev3FD99u55nXZCA63UVeN6TCEyPbJ7 # ugO2W1dj7PqzH3UKQCdfT0nb97V5XibtCC1e993X9PhgPvTTnpTEgevo4w2WgI96 # LVsONtMl9wX6yoyhziflYCLHn1idD4aOUNln7ruRlA227J3EE6YIEwy70idQZ1yT # xgeqgRTq1mHkqLfTfVsAmxfDkeBaP/AAfP659cRz7Sr/mbhHU5tkC3E1QwTAxM1T # FFrevcCrUchJY+5wKnqnMzRzDRsXq2yFHz4W0kmQ+HNpX1GvLMrYeq5w/pl1O8uI # XTmvkdEcb4Y/xobjysG6WVSEoYIYETCCGA0GCisGAQQBgjcDAwExghf9MIIX+QYJ # KoZIhvcNAQcCoIIX6jCCF+YCAQMxDzANBglghkgBZQMEAgEFADCCAWIGCyqGSIb3 # DQEJEAEEoIIBUQSCAU0wggFJAgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIB # BQAEIE9/RtEFwZJYZ9MvcHhm9jb6LCcnYvRCmPBpyZImn7PGAgZpwmaWceEYEzIw # MjYwNDA1MTUyMzE4LjQ3OVowBIACAfSggeGkgd4wgdsxCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNh # IE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjpBNTAwLTA1RTAt # RDk0NzE1MDMGA1UEAxMsTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGlu # ZyBBdXRob3JpdHmggg8hMIIHgjCCBWqgAwIBAgITMwAAAAXlzw//Zi7JhwAAAAAA # BTANBgkqhkiG9w0BAQwFADB3MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMUgwRgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRpdHkgVmVy # aWZpY2F0aW9uIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAwHhcNMjAx # MTE5MjAzMjMxWhcNMzUxMTE5MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVi # bGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQAD # ggIPADCCAgoCggIBAJ5851Jj/eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZHd5HBXXBv # f7KrQ5cMSqFSHGqg2/qJhYqOQxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv3lYkuLDs # fMuIEqvGYOPURAH+Ybl4SJEESnt0MbPEoKdNihwM5xGv0rGofJ1qOYSTNcc55EbB # T7uq3wx3mXhtVmtcCEr5ZKTkKKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgCsJ+sozf5 # EeH5KrlFnxpjKKTavwfFP6XaGZGWUG8TZaiTogRoAlqcevbiqioUz1Yt4FRK53P6 # ovnUfANjIgM9JDdJ4e0qiDRm5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5rO9yhv2fj # JHrmlQ0EIXmp4DhDBieKUGR+eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEfyTxgGjOs # RpeexIveR1MPTVf7gt8hY64XNPO6iyUGsEgt8c2PxF87E+CO7A28TpjNq5eLiiun # hKbq0XbjkNoU5JhtYUrlmAbpxRjb9tSreDdtACpm3rkpxp7AQndnI0Shu/fk1/rE # 3oWsDqMX3jjv40e8KN5YsJBnczyWB4JyeeFMW3JBfdeAKhzohFe8U5w9WuvcP1E8 # cIxLoKSDzCCBOu0hWdjzKNu8Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3rV69AgMB # AAGjggIbMIICFzAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYD # VR0OBBYEFGtpKDo1L0hjQM972K9J6T7ZPdshMFQGA1UdIARNMEswSQYEVR0gADBB # MD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0Rv # Y3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGC # NxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTI # ftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0wezB5oHegdYZzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRlbnRpdHkl # MjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHkl # MjAyMDIwLmNybDCBlAYIKwYBBQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAChnVodHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElk # ZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0 # aG9yaXR5JTIwMjAyMC5jcnQwDQYJKoZIhvcNAQEMBQADggIBAF+Idsd+bbVaFXXn # THho+k7h2ESZJRWluLE0Oa/pO+4ge/XEizXvhs0Y7+KVYyb4nHlugBesnFqBGEdC # 2IWmtKMyS1OWIviwpnK3aL5JedwzbeBF7POyg6IGG/XhhJ3UqWeWTO+Czb1c2NP5 # zyEh89F72u9UIw+IfvM9lzDmc2O2END7MPnrcjWdQnrLn1Ntday7JSyrDvBdmgbN # nCKNZPmhzoa8PccOiQljjTW6GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1I64XNGqs # t8S+w+RUdie8fXC1jKu3m9KGIqF4aldrYBamyh3g4nJPj/LR2CBaLyD+2BuGZCVm # oNR/dSpRCxlot0i79dKOChmoONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG89LDKbRS # SvijmwJwxRxLLpMQ/u4xXxFfR4f/gksSkbJp7oqLwliDm/h+w0aJ/U5ccnYhYb7v # PKNMN+SZDWycU5ODIRfyoGl59BsXR/HpRGtiJquOYGmvA/pk5vC1lcnbeMrcWD/2 # 6ozePQ/TWfNXKBOmkFpvPE8CH+EeGGWzqTCjdAsno2jzTeNSxlx3glDGJgcdz5D/ # AAxw9Sdgq/+rY7jjgs7X6fqPTXPmaCAJKVHAP19oEjJIBwD1LyHbaEgBxFCogYSO # iUIr0Xqcr1nJfiWG2GwYe6ZoAF1bMIIHlzCCBX+gAwIBAgITMwAAAFZ+j51YCI7p # YAAAAAAAVjANBgkqhkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGlj # IFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAyMDAeFw0yNTEwMjMyMDQ2NTFaFw0yNjEw # MjIyMDQ2NTFaMIHbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQ # MA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQL # Ex5uU2hpZWxkIFRTUyBFU046QTUwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jv # c29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5MIICIjANBgkq # hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtKWfm/ul027/d8Rlb8Mn/g0QUvvLqY2V # sy3tI8U2tFSspTZomZOD3BHT8LkR+RrhMJgb1VjAKFNysaK9cLSXifPGSIBrPCgs # 9P4y24lrJEmrV6Q5z4BmqMhIPrZhEvZnWpCS4HO7jYSei/nxmC7/1Er+l5Lg3PmS # xb8d2IVcARxSw1B4mxB6XI0nkel9wa1dYb2wfGpofraFmxZOxT9eNht4LH0RBSVu # eba6ZNpjS/0gtfm7qiIiyP6p6PRzTTbMnVqsHnV/d/rW0zHx+Q+QNZ5wUqKmTZJB # 9hU853+2pX5rDfK32uNY9/WBOAmzbqgpEdQkbiMavUMyUDShmycIvgHdQnS207sT # j8M+kJL3tOdahPuPqMwsaCCgdfwwQx0O9TKe7FSvbAEYs1AnldCl/KHGZCOVvUNq # jyL10JLe0/+GD9/ynqXGWFpXOjaunvZ/cKROhjN4M5e6xx0b2miqcPii4/ii2Zhe # KallJET7CKlpFShs3wyg6F/fojQxQvPnbWD4Nyx6lhjWjwmoLcx6w1FSCtavLCly # 33BLRSlTU4qKUxaa8d7YN7Eqpn9XO0SY0umOvKFXrWH7rxl+9iaicitdnTTksAnR # jvekdKT3lg7lRMfmfZU8vXNiN0UYJzT9EjqjRm0uN/h0oXxPhNfPYqeFbyPXGGxz # aYUz6zx3qTcCAwEAAaOCAcswggHHMB0GA1UdDgQWBBS+tjPyu6tZ/h5GsyLvyz1H # +FNIWjAfBgNVHSMEGDAWgBRraSg6NS9IY0DPe9ivSek+2T3bITBsBgNVHR8EZTBj # MGGgX6BdhltodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNy # b3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAu # Y3JsMHkGCCsGAQUFBwEBBG0wazBpBggrBgEFBQcwAoZdaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0El # MjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3J0MAwGA1UdEwEB/wQCMAAwFgYD # VR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeAMGYGA1UdIARfMF0w # UQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9z # b2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBBAIwDQYJ # KoZIhvcNAQEMBQADggIBAA4DqAXEsO26j/La7Fgn/Qifit8xuZekqZ57+Ye+sH/h # RTbEEjGYrZgsqwR/lUUfKCFpbZF8msaZPQJOR4YYUEU8XyjLrn8Y1jCSmoxh9l7t # WiSoc/JFBw356JAmzGGxeBA2EWSxRuTr1AuZe6nYaN8/wtFkiHcs8gMadxXBs6Dx # Vhyu5YnhLPQkfumKm3lFftwE7pieV7f1lskmlgsC6AeSGCzGPZUgCvcH5Tv/Qe9z # 7bIImSD3SuzhOIwaP+eKQTYf67TifyJKkWQSdGfTA6Kcu41k8LB6oPK+MLk1jbxx # K5wPqLSL62xjK04SBXHEJSEnsFt0zxWkxP/lgej1DxqUnmrYEdkxvzKSHIAqFWSZ # ul/5hI+vJxvFPhsNQBEk4cSulDkJQpcdVi/gmf/mHFOYhDBjsa15s4L+2sBil3XV # /T8RiR66Q8xYvTLRWxd2dVsrOoCwnsU4WIeiC0JinCv1WLHEh7Qyzr9RSr4kKJLW # dpNYLhgjkojTmEkAjFO774t3xB7enbvIF0GOsV19xnCUzq9EGKyt0gMuaphKlNjJ # +aTpjWMZDGo+GOKsnp93Hmftml0Syp3F9+M3y+y6WJGUZoIZJq227jDjjEndtpUr # h9BdPdVIfVJD/Au81Rzh05UHAivorQ3Os8PELHIgiOd9TWzbdgmGzcILt/ddVQER # MYIHQzCCBz8CAQEweDBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1l # c3RhbXBpbmcgQ0EgMjAyMAITMwAAAFZ+j51YCI7pYAAAAAAAVjANBglghkgBZQME # AgEFAKCCBJwwEQYLKoZIhvcNAQkQAg8xAgUAMBoGCSqGSIb3DQEJAzENBgsqhkiG # 9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYwNDA1MTUyMzE4WjAvBgkqhkiG9w0B # CQQxIgQgDQVLNtCFuFhKDetWwZ7S3QTAlZXzso2TE4Wi6VOg0gEwgbkGCyqGSIb3 # DQEJEAIvMYGpMIGmMIGjMIGgBCC2DDMlTaTj8JV3iTg5Xnpe4CSH60143Z+X9o5N # BgMMqDB8MGWkYzBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3Rh # bXBpbmcgQ0EgMjAyMAITMwAAAFZ+j51YCI7pYAAAAAAAVjCCA14GCyqGSIb3DQEJ # EAISMYIDTTCCA0mhggNFMIIDQTCCAikCAQEwggEJoYHhpIHeMIHbMQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQg # QW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTUw # MC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUg # U3RhbXBpbmcgQXV0aG9yaXR5oiMKAQEwBwYFKw4DAhoDFQD/c/cpFSqQWYBeXggy # RJ2ZbvYEEaBnMGWkYzBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1l # c3RhbXBpbmcgQ0EgMjAyMDANBgkqhkiG9w0BAQsFAAIFAO18tu8wIhgPMjAyNjA0 # MDUxMDI0NDdaGA8yMDI2MDQwNjEwMjQ0N1owdDA6BgorBgEEAYRZCgQBMSwwKjAK # AgUA7Xy27wIBADAHAgEAAgIkAjAHAgEAAgISXTAKAgUA7X4IbwIBADA2BgorBgEE # AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag # MA0GCSqGSIb3DQEBCwUAA4IBAQAggD4/bR6WgBGv4Ubhusl7gtfnsakpJmcxZg7Q # LBzG5axESnQp24WQ3g5LGsL7kC0n9VqMXD14lakstjEiDceNce5Y59ScFbIlOkcJ # 9CVKmtnmz6NeDyMrFBbQB7lLo56fcJoHbEqhhvMYihXOl9xzEHU2TfFHt77gOogl # Mf4o6QauMv3osWwcDeFfNZyseAzGVNQHYvPWrf8Kybe4YGnnOG3A7+tZB7tjOWtZ # pYDkBY/yXxapAUztVStG695ELqjpToL1QCj+H7KAUDA51Ecy6ONASlZJJH2+Brbt # Khd5ySrIXvFpFEVDyyJeYC+cr+Tti/RO8sNlAgKfTkEblJyrMA0GCSqGSIb3DQEB # AQUABIICAF6Kkag7B4gnORyKuxNCg9y61eKOBi5Fws/tHHZ3Nflwvi8uHL094XI9 # 73T9efzwL+GmDVjTjLLCALpfzDH79D58IS2Z2ONcB5aHAB/rnPVElC09oFN0/4mc # iI8UDsldt8D/zO5ztDtbjuedDoAohBzIjZ1hBRpgvZcfxxsoWnZ1hf/HtSrFBxy7 # h7OuDcI+aW+/yVMnFKGT3tQxft6ukD4VKuoc6umx2JfUw90PEaoM2HLN3weZldRL # +hksvEjUNDSVEKBCxjTZIzBVzBoJ0MEi4Pl4CKSDPwUj7z9PBOIAmb9/nxzP6U7K # q9FsMUAUgzHhMI2G/Ijs4BJSZeHJTOpzOZtNBnrC7lCqgHImZs3G60IdOC2EF6/K # 2kjTBKg9hwPTFeP252phHBVrSS9bxwcVs2sJ1xIU+5TVAJ7CaPI9tq5Mzz4uEfhP # 6sBZLPSIzZfZjqWBTqIZZigDGQa3pmus0S8lk7OgZAuNrYr7/pyCkOWw8xBWasGL # oJ2JtmlrDqo9AY7mtaFaD8Pf9RxcczfE0Yb7huXs2x/smEmGq7RnS90aBguBxfeZ # BopSELA97y3rEyiHZNpa946Nl9JcfllX1TUf8tBQ55pOWzbg93ZJo9cfDM32rSp6 # QXxap1+XhPkQpEfODo6M35W7fQ35TsSnd79sear4sjKyeZZdB4hS # SIG # End signature block |