计划自定义凭据提供程序

自定义凭据提供程序使 Windows 调试器能够对使用特定身份验证方法的符号和源服务器进行身份验证。 本文介绍如何实现通过 DLL、可执行文件或脚本实现的自定义提供程序。

学习内容:

  • 使用 XML 配置凭据提供程序
  • 将提供程序实现为 DLL 或可执行文件
  • 测试和排查自定义提供程序问题

本文介绍如何为 Windows 调试器编程自定义凭据提供程序。 使用此提供程序,您可以使用其他需要特定身份验证类型的符号和源服务器。 自定义支持服务器所需的任何类型的身份验证。

有两个选项:

  • 通过可执行文件(或通过 CMD/BAT 脚本启动)实现的自定义提供程序。
  • 使用本文档中所述的 API 接口实现的 DLL 自定义提供程序。

Windows 调试器 HTTPS 身份验证请求

Windows 调试器从符号服务器请求符号。 如果符号服务器不需要身份验证,调试器将返回符号,而无需使用任何凭据提供程序。 当符号服务器返回 HTTP 401(未授权访问/访问被拒绝)状态代码时,它表示需要身份验证。 这意味着请求缺少目标资源的有效身份验证凭据,并且服务器拒绝满足请求,直到客户端提供必要的身份验证信息。

如果配置自定义凭据提供程序,调试器将使用它返回的凭据重试失败的请求。 下一部分介绍如何配置自定义凭据提供程序。

自定义凭据提供程序的 XML 配置

若要配置自定义凭据提供程序,需要两个 XML 文件:

  1. DbgCredentialProvider.config.xml - 指定配置文件所在的位置
  2. 提供程序配置 XML - 定义提供程序实现(DLL、EXE 或脚本)

返回 401 未经授权的错误时,调试器将调用 DbgCredentialProvider.dll。 此 DLL 通过打开名为 DbgCredentialProvider.config.xml的文件来搜索凭据提供程序,该文件应与 DbgCredentialProvider.dll位于同一目录中。 此文件提供配置 XML 文件的文件夹位置。

DbgCredentialProvider.config.xml 搜索行为

调试器按以下顺序搜索 DbgCredentialProvider.config.xml,在找到文件时停止:

  1. 如果您设置了 DBG_COMMON_FOLDER 环境变量,则为其指定的文件夹。
  2. %LOCALAPPDATA%\Dbg\Common
  3. 调用应用程序的文件夹。
  4. 包含 DbgCredentialProvider.dll的文件夹。

注释

DBG_COMMON_FOLDER 是一个环境变量,指定调试器配置文件的文件夹位置。 设置时,调试器会先搜索此位置DbgCredentialProvider.config.xml再检查任何默认位置。

有关如何设置 WinDbg 相关环境值的信息,请参阅 环境变量

XML 配置文件位置 - DbgCredentialProvider.config.xml

下面显示了随调试器一起安装的默认 DbgCredentialProvider.config.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<!--
The config file is located next to the DbgCredentialProvider.dll.
-->
<Settings>
    <Folders>
        <!--
        This is a list of the folders which should be provided as an absolute file path or
        relative to the location of this config file.
        -->
        <Folder>CredentialProviders</Folder>
    </Folders>
</Settings>

可以在<Folders>元素下列出多个文件夹。 每个文件夹路径可以是相对路径,也可以是绝对路径。 相对路径的解析基于 DbgCredentialProvider.config.xml 文件所在的位置。 调试器按文件夹列出的顺序搜索提供程序。

在前面的示例中, <Folders> 集合包含一个文件夹 CredentialProviders,指定为相对路径。

自定义凭据提供程序的 XML 配置信息

读取 DbgCredentialProvider.config.xml后,调试器会枚举每个列出的文件夹中具有 .xml 扩展名的所有文件。 在前面的示例中,仅枚举 CredentialProviders 文件夹。 这些 XML 文件描述调试器可以使用哪些凭据提供程序,包括其位置(DLL、EXE 或 CMD/BAT 脚本)。 可以将位置指定为相对路径或绝对路径。 调试器支持多个自定义凭据提供程序。 它会向每个提供程序请求凭据,并使用第一个返回成功的提供程序中的凭据。

注释

未指定 XML 文件枚举的顺序。

示例 DbgCredentialProvider_gcmw.xml 文件显示了如何调用批处理文件。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <!--
          This is a list of the provider modules which should be provided as an absolute file path or
          relative to the location of this config file.
          The provider is a DLL, EXE or CMD file.
    -->
    <CredentialProvider>PATCredentialProvider\PATCredentialProvider.bat</CredentialProvider>

</CredentialProviders>

此示例 XML 演示如何配置要用作凭据提供程序的 DLL。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <!--
      This is a list of the provider modules which should be provided as an absolute file path or
      relative to the location of this config file.
      The provider is a DLL, EXE or CMD file.
    -->
   <CredentialProvider>GCMW\DbgCredentialProvider_gcmw.dll</CredentialProvider>
</CredentialProviders>

在此示例中,只有一个提供程序(DbgCredentialProvider_gcmw.dll)位于 GCMW 文件夹中,相对于 DbgCredentialProvider_gcmw.xml 文件位置。

多个凭据提供程序

此示例显示了多个凭据提供者。 提供程序按列出的顺序使用,以获取所请求主机的凭据。 一旦返回有效的令牌,该令牌就会返回到调试器,并且不使用以下令牌。

    <CredentialProviders> 

        <CredentialProvider ApiVersion="2.0.0" >GCMW\DbgCredentialProvider_gcmw.dll</CredentialProvider> 

        <CredentialProvider ApiVersion="2.0.0">OAuth2CredentialProvider\OAuth2CredentialProvider.cmd</CredentialProvider> 

    </CredentialProviders> 

使用命令行调用自定义凭据提供程序

本部分介绍如何将自定义凭据提供程序实现为 EXE 或 CMD/BAT 文件命令脚本。

如果将提供程序实现为 EXE 或 CMD 脚本,请将其设计为处理以下命令行参数(不区分大小写)。 不能合并这些参数。

  • 获取
  • Erase
  • Microsoft Store

获取命令

使用Get命令检索凭据。 提供程序通过标准输入流接收剩余数据。

标准输入流将其他输入数据传递给提供程序,后跟一个空行,用于标记输入参数的末尾。

参数不区分大小写,因此可以使用大写字母和小写字母的任意组合。

提供程序可以通过 error=zzzzz 返回错误。

Protocol=http or https
Host=xxx 
Path=yyy
ResourceKind=symbols or sources
Interactive=0 or 1
IsRetry=0 or 1
ParentHwnd=HWND
<empty line to mark the end of the input parameters>

获取参数

字段 类型 描述
协议 LPCWSTR HTTP 或 HTTPS。 若要提高安全性,请使用 HTTPS。
主机 LPCWSTR 主机服务器的名称,例如 contoso.symbols.com
路径 LPCWSTR 符号目录的路径,例如apis/symbol/symsrv。 调用方/调试器确保 Path 从不以“/”字符开头
ResourceKind LPCWSTR 它可以是“符号”或“源”。 将来可能会添加其他资源类型。 凭据提供程序实现使用此字段来调整获取凭据时所需的权限。 它还可用于缓存凭据以供将来使用。
交互 布尔 true = 可以显示 UI,false = 无 UI。
IsRetry 布尔 如果 为 true,提供程序必须跳过读取缓存并获取新凭据。
ParentHwnd HWND 如果显示身份验证 UI,则此为父窗口句柄(HWND),例如 0x%I64x。 应用程序可以使用DBG_CREDENTIAL_PROVIDER_PARENT_HWND环境变量或 imagehlp/dbghelp SymSetParentWindow 方法设置父 HWND。

通过使用列出的参数连接<protocol>://<host>/<path>来生成完整的 URI/URL,例如: https://contoso.symbols.com/apis/symbol/symsrv 请求如下所示:

protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
resourceKind=symbols
isretry=false 
issilent=false
parenthwnd=593598
<Followed by an empty line to indicate the end of the input data.>

Erase

凭据提供程序可以使用此命令从其缓存中清除凭据。 输入参数与命令 Get 相同。 不需要输出返回值。 可能会返回错误。

Microsoft Store

凭据提供程序使用此命令将凭据存储在其缓存中。 输入参数与命令 Get 相同。 不需要输出返回值。 可能会返回错误。

本地令牌缓存和 isRetry

对于对提供程序的初始请求,调试器将发送参数 isRetry=false。 某些提供程序可能会从其本地缓存中获取令牌。 调试器使用此令牌重新发送 HTTP 请求后,服务器可能会再次返回 401 响应。 此响应可能是因为令牌已过期。 然后,调试器会向凭据提供程序请求新的令牌,这次使用参数 isRetry=true。 在这种情况下,提供程序不应使用其缓存,而是检索全新的令牌。

交互式设置 - 身份验证 UI

在某些非交互式环境中(例如测试实验室),用户无法与 UI 交互。 在这种情况下,请将 issilent 参数设置为 true。 当此参数为 true 时,提供程序不应显示任何身份验证或其他 UI。

测试实验室或应用程序中的脚本可以使用以下选项来控制交互式标志:

设置无提示(非交互式)符号服务器

使用 SymbolServerSetOptions 函数 设置无提示(非交互式)符号服务器。 如果设置为 SSRVOPT_UNATTENDED TRUE,SymSrv 不会显示对话框或弹出窗口。 如果将值设置为 FALSE,SymSrv 会在建立连接时显示这些图形功能。

管理用户界面窗口

某些凭据提供程序显示身份验证界面。 如果是这样,应该使用ParentHwnd 参数,以便此 UI 作为模式对话框显示在调试器主窗口中。 否则,身份验证 UI 可能隐藏在主调试器窗口后面,用户可能会认为调试器没有响应。

与 WinDbg 类似的调试器客户端应用程序可以使用 DBG_CREDENTIAL_PROVIDER_PARENT_HWND 环境变量或 imagehlp/dbghelp SymSetParentWindow 方法设置父 HWND。 您也可以使用IDebugAdvanced2::Request 信息DEBUG_REQUEST_SET_PARENT_HWND,将 HWND 值转换为 UINT32。

退回值要求

EXE 或 CMD/BAT 脚本必须通过输出流返回用户名和密码,如下所示:

username=aaa
password=bbb - where the password can be a password or PAT

CredentialKind

身份验证请求返回 CredentialKind。 CredentialKind 有两个选项。

  • 基本身份验证:RFC 7617 中定义。 系统以使用 Base64 编码的用户 ID 和密码对的形式传输凭据。
username=xxx
credentialkind=Basic
password=yyy --> This can be a password or a PAT token
  • 持有者身份验证:在 RFC 6750 中定义。 系统在 HTTP 请求中使用持有者令牌来访问 OAuth 2.0 受保护的资源。
username=xxx
credentialkind=Bearer
header=Bearer <TOKEN_GOES_HERE> ---> Usually OAuth2 tokens begin with "ey" and it is a very long string

CMD 文件的示例

以下示例显示了返回 HTTP 身份验证标头的 CMD 文件:

CredentialProviders 文件夹中的 OAuth2CredentialProvider.xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
    <CredentialProvider ApiVersion="2.0.0" >OAuth2CredentialProvider\OAuth2CredentialProvider.cmd</CredentialProvider>
</CredentialProviders>	

OAuth2CredentialProvider.cmd位于 OAuth2CredentialProvider 文件夹中的文件:

@echo off
echo username=UserName@domain.com
echo header=Bearer <TOKEN_GOES_HERE>

测试自定义提供程序

如果编写位于 CMD 或 EXE 文件中的自定义提供程序,可以通过从控制台窗口启动它来测试它。 例如:

DebuggerCredentialManager.exe Get

此命令启动应用程序。 应用程序会打印一些信息,然后等待用户输入。 空行指示用户输入的末尾。

[Information] [DebuggerCredentialProvider.102949]Microsoft Debugger Credential Manager version 2024.0409.02656.285 (Windows, .NET 6.0.29) 'get'

下面是在控制台窗口输入流中输入的信息的示例。 可以在大写和小写的任意组合中输入它。

protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
resourceKind=symbols
isretry=false 
issilent=false
parenthwnd=593598

然后按 Enter 键两次发送空白行并指示用户输入的结束。

提供程序通过标准输出流进行响应。

[Verbose] [DebuggerCredentialProvider.103258]AzureCredentialProvider - Attempting to acquire bearer token using provider 'Msal Cache'
[Verbose] [DebuggerCredentialProvider.103300]Token expiration data - current UTC time:9/18/2024 5:33:00 PM, ExpiresOn: 9/18/2024 6:43:25 PM
[Information] [DebuggerCredentialProvider.103300]AzureCredentialProvider - Acquired bearer token using 'Msal Cache'
protocol=https
host=contoso.symbols.com
path=apis/symbol/symsrv
username=UserName@domain.com
credentialkind=Bearer
header=Bearer eyJ0eXAi....
<empty line>

调试器忽略与模式 key=value不匹配的任何行。 密钥必须是以下值之一:协议、主机、路径、用户名、credentialkind 或标头。

调试器忽略键值对中的大小写。 它将空白行视为输入的末尾。

提供程序诊断信息

提供程序可以在输出流上打印诊断信息。 调试器将忽略此信息,不会向用户显示此信息。 此处显示的额外信息示例仅用于说明目的。 其他提供程序可能会打印不同的诊断信息,或者不打印任何信息。

返回 PAT 令牌的 PowerShell 示例脚本

下面是返回 PAT 令牌的 PowerShell 脚本示例。

文件 PatCredentialProvider.xml 将 PATCredentialProvider.bat 配置为 CredentialProvider。

<?xml version="1.0" encoding="utf-8"?>
<CredentialProviders>
     <CredentialProvider ApiVersion="2.0.0">PATCredentialProvider\PATCredentialProvider.bat</CredentialProvider>
</CredentialProviders>

文件 PATCredentialProvider.bat 位于 PATCredentialProvider 文件夹中,并调用 PATCredentialProvider.ps1。

@echo off
<PATH_TO_POWERSHELL>\PowerShell.exe -NoProfile -executionpolicy Unrestricted -WindowStyle Hidden -File "%~dp0\PATCredentialProvider.ps1"

PATCredentialProvider.ps1 文件也位于 PATCredentialProvider 文件夹中。

<#
 .SYNOPSIS
    Given input, parses to find out which symbol server we want credentials for, and searches the Microsoft Credential Manager for those credentials.
    If found, prints the credentials to standard output. If not, prints error.

 .INPUT
    Delivered through standard input:
    protocol=http or https
    host=xxx ex. host=contoso.symbols.com
    path=yyy ex. path=apis/symbol/symsrv
    resourceKind=symbols
    isretry=false 
    issilent=false
    parenthwnd=593598
    <empty line to mark the end of the input parameters>

 .OUTPUT

    Delivered through standard output:

    username=aaa
    password=bbb - where the password can be a password or PAT. When PAT is returned the username will be any name (not necessarily the name of the currently logged in user)<!--[SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="It's an example")]-->
    <empty line to mark the end of the output parameters>

 #>

 $logDirectory = (Get-Item Env:LoggingDirectory).Value
 $logFile = Join-Path $logDirectory "credProviderLog.txt"

 try
 {
    "Entering Credential Provider" | Out-File $logFile -Append

    $lines = While($line=Read-Host) {$line}
    $lines | Out-File $logFile -Append
    if (!(Get-Module "CredentialManager"))
    {
        "Installing module" | Out-File $logFile -Append

        Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

        Install-Module CredentialManager -force -Scope CurrentUser
    }

    $pathLine = $lines | Where-Object {$_.StartsWith("path=")} | Select-Object -First 1
    "Found path line: $pathLine" | Out-File $logFile -Append

    [regex]$regex="path=(?<ServerName>.*)"
    $pathLine -Match $regex

    $symbolPath = "symbol:$($Matches.ServerName)"

    "Found symbol path: $symbolPath" | Out-File $logFile -Append

    $PAT = Get-StoredCredential -Target $symbolPath -AsCredentialObject

    if ($PAT)
    {
        "Found PAT!" | Out-File $logFile -Append
        Write-Host "username=placeholder"
        Write-Host "password=$($PAT.Password)"; # For OAuth 2 tokens You can change to output header=Bearer TOKEN
        Write-Host
    }
    else
    {
        "Could not locate PAT for Symbol Server: $symbolPath" | Out-File $logFile -Append
        Write-Host "error=Could not locate PAT for Symbol Server: $symbolPath"
    }
 }
 catch [System.SystemException]
 {
    "ERROR" | Out-File $logFile -Append
 
    $_ | Out-File $logFile -Append
 }

以 DLL 形式实现的凭证提供程序的 C++ API

概述

以下说明介绍公共接口凭据提供程序在 DLL 中实现自定义凭据提供程序时必须使用它们。 如果在 DLL 中实现提供程序,则必须导出 GetUserCredentials 方法。 这些方法位于 namespace Debugger::CredentialProvider::Provider.

所需的 DbgCredentialProviderImpl.h 头文件随 Windows SDK 一起发布。 有关下载 SDK 的信息,请参阅 Windows SDKSDK Insider Preview

消息处理

// The caller of this method should not terminate the message with '\r' or '\n' characters
// (which makes this method similar to PrintLine)
typedef void (*DbgPrintMessageFn)(_In_ MessageErrorLevelKind errorLevel, _In_ PCWSTR message);

enum class MessageErrorLevelKind : uint8_t
{
    Info = 0,
    Warning,
    Error
};

响应类型

enum CredentialResponseResultKind
{
    Success = 0,
    NoProviders, // There are no installed providers or can't launch 3rd party provider
    ProviderNotApplicable, // the provider can't handle requests to the provided URL
    Error
};

请求结构

结构存储GetUserCredentialsRequest。 它使用与上表中所述的参数相同的参数。

struct GetUserCredentialsRequest
{
    LPCWSTR Protocol;      // The full request URL can be built from Protocol, Host and Path as follows:
    LPCWSTR Host;          // Protocol://Host/Path
    LPCWSTR Path;          // The caller/debugger will make sure Path never starts with '/' character
    LPCWSTR ResourceKind;  // It can be "symbols", "sources", etc.
                           // The credential provider implementation may use this to adjust
                           // the required permissions when acquiring credentials. It also may be used
                           // to cache credentials for future use.
    bool Interactive;      // true - display UI is ok, false - no UI.
                           // Refer to the explanations above on how to setup a non interactive environment
    bool IsRetry;          // When true the provider may skip reading the caches and get new credentials
    HWND ParentHwnd;       // The parent HWND if an authentication UI is displayed.
                           // The applications can use DBG_CREDENTIAL_PROVIDER_PARENT_HWND environment variable
                           // or imagehlp/dbghelp SymSetParentWindow method to setup the parent HWND.

    DbgPrintMessageFn PrintMessageFn;
}

使用凭据

函数 GetUserCredentials 请求凭据,以便在 HTTP 请求中向符号服务器发送符号。

HRESULT WINAPI GetUserCredentials(
  _In_ GetUserCredentialsRequest const & request,
  _Inout_ GetUserCredentialsResponse * pResponse);

此方法的调用方(调试器)提供请求和响应参数。 调用方确保在方法入口时UserNamePasswordErrorMessagenullptr

实现填充UserNamePasswordErrorMessage(可选)和Result


class GetUserCredentialsResponse final
{
public:

    CredentialResponseResultKind Result = CredentialResponseResultKind::Error;
    BSTR UserName = nullptr;
    BSTR Password = nullptr;
    BSTR HttpAuthenticationHeader = nullptr;
    BSTR ErrorMessage = nullptr;

    GetUserCredentialsResponse() = default;

    GetUserCredentialsResponse(GetUserCredentialsResponse const&) = delete;
    GetUserCredentialsResponse& operator=(GetUserCredentialsResponse const&) = delete;

    GetUserCredentialsResponse(GetUserCredentialsResponse&& other) noexcept
    {
        Result = other.Result;
        UserName = other.UserName;
        Password = other.Password;
        HttpAuthenticationHeader = other.HttpAuthenticationHeader;
        ErrorMessage = other.ErrorMessage;

        other.Release();
    }

    GetUserCredentialsResponse& operator=(GetUserCredentialsResponse&& other) noexcept
    {
        if (this != addressof(other))
        {
            Clear();

            Result = other.Result;
            UserName = other.UserName;
            Password = other.Password;
            HttpAuthenticationHeader = other.HttpAuthenticationHeader;
            ErrorMessage = other.ErrorMessage;

            other.Release();
        }
        return (*this);
    }

    ~GetUserCredentialsResponse()
    {
        Clear();
    }

    void Clear()
    {
        SecureSysFreeString(UserName);
        SecureSysFreeString(Password);
        SecureSysFreeString(HttpAuthenticationHeader);
        SecureSysFreeString(ErrorMessage);
    }

private:
    template <class _Tp>
    _Tp* addressof(_Tp& __x) noexcept
    {
        return reinterpret_cast<_Tp*>(
            const_cast<char*>(&reinterpret_cast<const volatile char&>(__x)));
    }

    void Release() noexcept
    {
        Result = CredentialResponseResultKind::Error;
        UserName = nullptr;
        Password = nullptr;
        ErrorMessage = nullptr;
        HttpAuthenticationHeader = nullptr;
    }

    void SecureSysFreeString(_Inout_ BSTR& bstrString)
    {
        if (bstrString != nullptr)
        {
            size_t const length = wcslen(bstrString);
            SecureZeroMemory(bstrString, length * sizeof(wchar_t));
            SysFreeString(bstrString);
            bstrString = nullptr;
        }
    }
};

后续步骤

了解自定义凭据提供程序后,请尝试以下任务:

相关文章: