SSPI WDigest 服务器验证失败
SSPI WDigest Server Vaildation Failure
我正在尝试让 Win32 SSPI API 验证 challenge response from a client. The call to AcceptSecurityContext 总是失败并显示 SEC_E_INVALID_TOKEN (0x80090308) 或 SEC_E_INTERNAL_ERROR (0x80090304)。
我已将问题简化为此示例代码:
#define SECURITY_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <security.h>
#include <wdigest.h>
#include <cstdlib>
#include <string>
#include <iostream>
#pragma comment(lib, "Secur32.lib")
using namespace std::string_literals;
int main()
{
auto const realm = L"realm"s;
auto const client_method = "GET"s;
auto const client_target = L"HTTP/TARGETMACHINE"s;
auto const client_uri = L"/"s;
auto const client_uri_utf8 = "/"s;
// server generate digest challenge
PSecPkgInfoW package_info;
auto result = QuerySecurityPackageInfoW(const_cast<LPWSTR>(WDIGEST_SP_NAME_W), &package_info);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
CredHandle serverCredHandle;
TimeStamp lifetime;
result = AcquireCredentialsHandleW(nullptr, const_cast<LPWSTR>(WDIGEST_SP_NAME_W), SECPKG_CRED_INBOUND, nullptr, nullptr, nullptr, nullptr, &serverCredHandle, &lifetime);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
SecBuffer challengeInBuffers[5];
// token
challengeInBuffers[0].BufferType = SECBUFFER_TOKEN;
challengeInBuffers[0].cbBuffer = 0;
challengeInBuffers[0].pvBuffer = nullptr;
// method
challengeInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[1].cbBuffer = 0;
challengeInBuffers[1].pvBuffer = nullptr;
// uri
challengeInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[2].cbBuffer = 0;
challengeInBuffers[2].pvBuffer = nullptr;
// body hash
challengeInBuffers[3].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[3].cbBuffer = 0;
challengeInBuffers[3].pvBuffer = nullptr;
// realm
challengeInBuffers[4].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[4].cbBuffer = realm.size() * sizeof(wchar_t);
challengeInBuffers[4].pvBuffer = const_cast<void*>(static_cast<void const*>(realm.c_str()));
SecBufferDesc challengeInBufferDesc;
challengeInBufferDesc.ulVersion = 0;
challengeInBufferDesc.cBuffers = 5;
challengeInBufferDesc.pBuffers = challengeInBuffers;
std::string challenge;
challenge.resize(package_info->cbMaxToken);
SecBuffer challengeOutBuffer;
challengeOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeOutBuffer.cbBuffer = challenge.size();
challengeOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(challenge.data()));
SecBufferDesc challengeOutBufferDesc;
challengeOutBufferDesc.ulVersion = 0;
challengeOutBufferDesc.cBuffers = 1;
challengeOutBufferDesc.pBuffers = &challengeOutBuffer;
CtxtHandle serverContextHandle;
unsigned long outContextAttributes;
result = AcceptSecurityContext(&serverCredHandle, nullptr, &challengeInBufferDesc, 0, SECURITY_NETWORK_DREP, &serverContextHandle, &challengeOutBufferDesc, &outContextAttributes, &lifetime);
if (result != SEC_I_CONTINUE_NEEDED)
{
return EXIT_FAILURE;
}
challenge.resize(challengeOutBuffer.cbBuffer);
std::cout << "Challenge: [" << challenge << "]\n";
// client challenge response generation
SEC_WINNT_AUTH_IDENTITY_W auth_data;
auth_data.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
auth_data.User = nullptr;
auth_data.UserLength = 0;
auth_data.Domain = nullptr;
auth_data.DomainLength = 0;
auth_data.Password = nullptr;
auth_data.PasswordLength = 0;
CredHandle clientCredHandle;
result = AcquireCredentialsHandleW(nullptr, const_cast<LPWSTR>(WDIGEST_SP_NAME_W), SECPKG_CRED_OUTBOUND, nullptr, &auth_data, nullptr, nullptr, &clientCredHandle, &lifetime);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
SecBuffer challengeResponseInBuffers[4];
// token
challengeResponseInBuffers[0].BufferType = SECBUFFER_TOKEN;
challengeResponseInBuffers[0].cbBuffer = challenge.size();
challengeResponseInBuffers[0].pvBuffer = const_cast<void*>(static_cast<void const*>(challenge.data()));
// method
challengeResponseInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
challengeResponseInBuffers[1].cbBuffer = client_method.size();
challengeResponseInBuffers[1].pvBuffer = const_cast<void*>(static_cast<void const*>(client_method.data()));
// body hash
challengeResponseInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
challengeResponseInBuffers[2].cbBuffer = 0;
challengeResponseInBuffers[2].pvBuffer = nullptr;
// target
challengeResponseInBuffers[3].BufferType = SECBUFFER_STREAM;
challengeResponseInBuffers[3].cbBuffer = client_target.size() * sizeof(wchar_t);
challengeResponseInBuffers[3].pvBuffer = const_cast<void*>(static_cast<void const*>(client_target.data()));
SecBufferDesc challengeResponseInBufferDesc;
challengeResponseInBufferDesc.ulVersion = 0;
challengeResponseInBufferDesc.cBuffers = 4;
challengeResponseInBufferDesc.pBuffers = challengeResponseInBuffers;
std::string challengeResponse;
challengeResponse.resize(package_info->cbMaxToken);
SecBuffer challengeResponseOutBuffer;
challengeResponseOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeResponseOutBuffer.cbBuffer = challengeResponse.size();
challengeResponseOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(challengeResponse.data()));
SecBufferDesc challengeResponseOutBufferDesc;
challengeResponseOutBufferDesc.ulVersion = 0;
challengeResponseOutBufferDesc.cBuffers = 1;
challengeResponseOutBufferDesc.pBuffers = &challengeResponseOutBuffer;
CtxtHandle clientContextHandle;
result = InitializeSecurityContextW(&clientCredHandle, nullptr, const_cast<SEC_WCHAR*>(client_uri.c_str()), 0, 0, SECURITY_NETWORK_DREP, &challengeResponseInBufferDesc, 0, &clientContextHandle, &challengeResponseOutBufferDesc, &outContextAttributes, &lifetime);
if(result != SEC_E_OK)
{
return EXIT_FAILURE;
}
challengeResponse.resize(challengeResponseOutBuffer.cbBuffer);
std::cout << "Challenge Response: [" << challengeResponse << "]\n";
// server verify challenge response
SecBuffer verifyChallengeResponseInBuffers[5];
// token
verifyChallengeResponseInBuffers[0].BufferType = SECBUFFER_TOKEN;
verifyChallengeResponseInBuffers[0].cbBuffer = challengeResponse.size();
verifyChallengeResponseInBuffers[0].pvBuffer = const_cast<void*>(static_cast<void const*>(challengeResponse.data()));
// method
verifyChallengeResponseInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[1].cbBuffer = client_method.size();
verifyChallengeResponseInBuffers[1].pvBuffer = const_cast<void*>(static_cast<void const*>(client_method.data()));
// uri
verifyChallengeResponseInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[2].cbBuffer = client_uri_utf8.size();
verifyChallengeResponseInBuffers[2].pvBuffer = const_cast<void*>(static_cast<void const*>(client_uri_utf8.data()));
// body hash
verifyChallengeResponseInBuffers[3].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[3].cbBuffer = 0;
verifyChallengeResponseInBuffers[3].pvBuffer = nullptr;
// realm
verifyChallengeResponseInBuffers[4].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[4].cbBuffer = realm.size() * sizeof(wchar_t);
verifyChallengeResponseInBuffers[4].pvBuffer = const_cast<void*>(static_cast<void const*>(realm.c_str()));
SecBufferDesc verifyChallengeResponseInBufferDesc;
verifyChallengeResponseInBufferDesc.ulVersion = 0;
verifyChallengeResponseInBufferDesc.cBuffers = 5;
verifyChallengeResponseInBufferDesc.pBuffers = verifyChallengeResponseInBuffers;
std::string verifyChallengeResponse;
verifyChallengeResponse.resize(package_info->cbMaxToken);
SecBuffer verifyChallengeResponseOutBuffer;
challengeOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeOutBuffer.cbBuffer = verifyChallengeResponse.size();
challengeOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(verifyChallengeResponse.data()));
SecBufferDesc verifyChallengeResponseOutBufferDesc;
verifyChallengeResponseOutBufferDesc.ulVersion = 0;
verifyChallengeResponseOutBufferDesc.cBuffers = 1;
verifyChallengeResponseOutBufferDesc.pBuffers = &verifyChallengeResponseOutBuffer;
result = AcceptSecurityContext(&serverCredHandle, &serverContextHandle, &verifyChallengeResponseInBufferDesc, 0, SECURITY_NETWORK_DREP, &serverContextHandle, &verifyChallengeResponseOutBufferDesc, &outContextAttributes, &lifetime);
if (result != SEC_I_COMPLETE_NEEDED)
{
std::cout << "Challenge Response Verification Failed with [0x" << std::hex << result << "]";
return EXIT_FAILURE;
}
std::cout << "Challenge Response Verified";
return EXIT_SUCCESS;
}
我得到的输出是:
Challenge:
[qop="auth",algorithm=MD5-sess,nonce="+Upgraded+v1db70e06e6d35dfd59df6fcd8d3cf7f7671d23e810144d5012972e89ccadaa398f05e8d5ab7a9c0eb3d52b00f4446530312ac45e30f4ac02c",charset=utf-8,realm="realm"]
Challenge Response:
[username="",realm="realm",nonce="+Upgraded+v1db70e06e6d35dfd59df6fcd8d3cf7f7671d23e810144d5012972e89ccadaa398f05e8d5ab7a9c0eb3d52b00f4446530312ac45e30f4ac02c",uri="/",cnonce="+Upgraded+v15c1d89757bd55776df6e2788dcdca9220f6aa1af03856d237a0e37a8136b5a44",nc=00000001,algorithm=MD5-sess,response="ee315ddeed6f2d3d68b6cafff9f7d52e",qop="auth",charset=utf-8,hashed-dirs="service-name,channel-binding",service-name="",channel-binding="00000000000000000000000000000000"]
Challenge Response Verification Failed with [0x80090308]
我已经尝试改变挑战/挑战响应和验证部分的输入,但没有实际结果。
MSDN 文档似乎没有多大帮助,我对这篇摘要的大部分见解 API 都来自此处的 .net 参考源:
https://referencesource.microsoft.com/#System/net/System/Net/_NTAuthentication.cs
有人知道我做错了什么吗?
更新:
我机器的输出,UseLogonCredential 设置为“1”(虽然我用它测试了“1”和“0”,输出完全相同)对于下面答案中的示例应用程序,我得到以下输出:
Capabilities: 0x800304
wVersion: 0x1
Max Token Size: 0x1000
SERVER: AcquireCredentialsHandle SUCCESS
CLIENT: AcquireCredentialsHandle SUCCESS
CLIENT: InitializeSecurityContext: SEC_I__CONTINUE_NEEDED
SERVER: AcceptSecurityContext: SEC_I__CONTINUE_NEEDED
CLIENT: InitializeSecurityContext: SEC_I_CONTINUE_NEEDED
AcceptSecurityContext SUCCESS
InitializeSecurityContext (2) failed with 0x8009030A
这对我来说似乎可以正常工作,但我不明白为什么您仍然需要最后一个客户端 InitializeSecurityContext,因为服务器端已经验证了它?
我也可以删除第一个 "CLIENT: InitializeSecurityContext",因为它不需要,returns 一个 "blank" 令牌,如果删除它仍然可以正常工作。
我无法开始使用客户端的当前用户凭据,我必须提供 SEC_WINNT_AUTH_IDENTITY 和有效凭据。这有什么原因吗?
默认情况下禁用 wdigest 包,因为它使用不安全的 MD5。
您必须在注册表中启用包才能使用该代码。 KB2871997 and Wdigest – Part 1。为什么要使用 wdigest,因为它不再安全了。
#include <windows.h>
#include <stdio.h>
#define SECURITY_WIN32
#include <sspi.h>
void wmain(int argc, WCHAR *argv[])
{
SECURITY_STATUS ss;
SecPkgInfo *spi = {0};
ss = QuerySecurityPackageInfo(L"wdigest", &spi);
if (ss != SEC_E_OK)
{
wprintf(L"QuerySecurityPackageInfo failed with %u\n", ss);
return;
}
wprintf(L"Capabilities: 0x%X\n", spi->fCapabilities);
wprintf(L"wVersion: 0x%X\n", spi->wVersion);
wprintf(L"Max Token Size: 0x%X\n", spi->cbMaxToken);
LPBYTE lpbOut = NULL;
lpbOut = (LPBYTE)LocalAlloc(LPTR, spi->cbMaxToken);
if (lpbOut == NULL)
{
wprintf(L"LocalAlloc failed with %u\n", GetLastError());
return;
}
CredHandle ServerCred;
CredHandle ClientCred;
TimeStamp LifeTime;
// server side
ss = AcquireCredentialsHandle(NULL, L"wdigest", SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &ServerCred, &LifeTime);
if (ss != SEC_E_OK)
{
wprintf(L"AcquireCredentialsHandle failed with %u\n", ss);
return;
}
else
wprintf(L"SERVER: AcquireCredentialsHandle SUCCESS\n");
// client side
SEC_WINNT_AUTH_IDENTITY AuthIdentity;
WCHAR szDomain[] = L"domain";
WCHAR szPassword[] = L"password";
WCHAR szUser[] = L"user";
AuthIdentity.Domain = (unsigned short *)szDomain;
AuthIdentity.DomainLength = (unsigned long)wcslen(szDomain);
AuthIdentity.Password = (unsigned short *)szPassword;
AuthIdentity.PasswordLength = (unsigned long)wcslen(szPassword);
AuthIdentity.User = (unsigned short *)szUser;
AuthIdentity.UserLength = (unsigned long)wcslen(szUser);
AuthIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
/*
AuthIdentity.Domain = (unsigned short *)NULL;
AuthIdentity.DomainLength = (unsigned long)0;
AuthIdentity.Password = (unsigned short *)NULL;
AuthIdentity.PasswordLength = (unsigned long)0;
AuthIdentity.User = (unsigned short *)NULL;
AuthIdentity.UserLength = (unsigned long)0;
AuthIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;*/
ss = AcquireCredentialsHandle(NULL, L"wdigest", SECPKG_CRED_OUTBOUND, NULL, &AuthIdentity, NULL, NULL, &ClientCred, &LifeTime);
if (ss != SEC_E_OK)
{
wprintf(L"OUTBOUND: AcquireCredentialsHandle failed with 0x%X\n", ss);
return;
}
else
wprintf(L"CLIENT: AcquireCredentialsHandle SUCCESS\n");
SecBufferDesc OutputBuffers;
SecBuffer TempTokensOut[6];
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
CtxtHandle ClientCtxtHandle;
ULONG ClientContextRetFlags = 0;
BOOL bContinue = FALSE;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY | */ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, NULL, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"CLIENT: InitializeSecurityContext: SEC_I__CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext failed with 0x%X\n", ss);
return;
}
}
SecBufferDesc InputBuffers;
SecBuffer TempTokensIn[10];
LPBYTE lpbIn = NULL;
lpbIn = (LPBYTE)LocalAlloc(LPTR, spi->cbMaxToken);
if (lpbIn == NULL)
{
wprintf(L"LocalAlloc failed with %u\n", GetLastError());
return;
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ULONG TargetDataRep = 0;
CtxtHandle ServerCtxtHandle;
ULONG ServerContextRetFlags = 0;
ss = AcceptSecurityContext(&ServerCred, NULL, &InputBuffers, 0 /*ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY*/, TargetDataRep, &ServerCtxtHandle, &OutputBuffers, &ServerContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"SERVER: AcceptSecurityContext: SEC_I__CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"AcceptSecurityContext failed with 0x%X\n", ss);
return;
}
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY | */ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, &InputBuffers, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"CLIENT: InitializeSecurityContext: SEC_I_CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext (2) failed with 0x%X\n", ss);
return;
}
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = AcceptSecurityContext(&ServerCred, NULL, &InputBuffers, 0 /*ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY*/, TargetDataRep, &ServerCtxtHandle, &OutputBuffers, &ServerContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
}
else if (ss != SEC_E_OK)
{
wprintf(L"AcceptSecurityContext (2) failed with 0x%X\n", ss);
return;
}
else
wprintf(L"AcceptSecurityContext SUCCESS\n");
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY |*/ ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, &InputBuffers, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
}
else if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext (2) failed with 0x%X\n", ss);
return;
}
else
wprintf(L"InitializeSecurityContext SUCCESS\n");
ss = FreeCredentialsHandle(&ClientCred);
if (ss != SEC_E_OK)
{
wprintf(L"FreeCredentialsHandle failed with %u\n", ss);
return;
}
ss = FreeCredentialsHandle(&ServerCred);
if (ss != SEC_E_OK)
{
wprintf(L"FreeCredentialsHandle failed with %u\n", ss);
return;
}
if (lpbOut != NULL)
LocalFree((HLOCAL)lpbOut);
if (lpbIn != NULL)
LocalFree((HLOCAL)lpbIn);
if (spi != NULL)
FreeContextBuffer((PVOID)spi);
}
当启用 wdigest 包时,这个示例可以工作吗?
我正在尝试让 Win32 SSPI API 验证 challenge response from a client. The call to AcceptSecurityContext 总是失败并显示 SEC_E_INVALID_TOKEN (0x80090308) 或 SEC_E_INTERNAL_ERROR (0x80090304)。
我已将问题简化为此示例代码:
#define SECURITY_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <security.h>
#include <wdigest.h>
#include <cstdlib>
#include <string>
#include <iostream>
#pragma comment(lib, "Secur32.lib")
using namespace std::string_literals;
int main()
{
auto const realm = L"realm"s;
auto const client_method = "GET"s;
auto const client_target = L"HTTP/TARGETMACHINE"s;
auto const client_uri = L"/"s;
auto const client_uri_utf8 = "/"s;
// server generate digest challenge
PSecPkgInfoW package_info;
auto result = QuerySecurityPackageInfoW(const_cast<LPWSTR>(WDIGEST_SP_NAME_W), &package_info);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
CredHandle serverCredHandle;
TimeStamp lifetime;
result = AcquireCredentialsHandleW(nullptr, const_cast<LPWSTR>(WDIGEST_SP_NAME_W), SECPKG_CRED_INBOUND, nullptr, nullptr, nullptr, nullptr, &serverCredHandle, &lifetime);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
SecBuffer challengeInBuffers[5];
// token
challengeInBuffers[0].BufferType = SECBUFFER_TOKEN;
challengeInBuffers[0].cbBuffer = 0;
challengeInBuffers[0].pvBuffer = nullptr;
// method
challengeInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[1].cbBuffer = 0;
challengeInBuffers[1].pvBuffer = nullptr;
// uri
challengeInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[2].cbBuffer = 0;
challengeInBuffers[2].pvBuffer = nullptr;
// body hash
challengeInBuffers[3].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[3].cbBuffer = 0;
challengeInBuffers[3].pvBuffer = nullptr;
// realm
challengeInBuffers[4].BufferType = SECBUFFER_PKG_PARAMS;
challengeInBuffers[4].cbBuffer = realm.size() * sizeof(wchar_t);
challengeInBuffers[4].pvBuffer = const_cast<void*>(static_cast<void const*>(realm.c_str()));
SecBufferDesc challengeInBufferDesc;
challengeInBufferDesc.ulVersion = 0;
challengeInBufferDesc.cBuffers = 5;
challengeInBufferDesc.pBuffers = challengeInBuffers;
std::string challenge;
challenge.resize(package_info->cbMaxToken);
SecBuffer challengeOutBuffer;
challengeOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeOutBuffer.cbBuffer = challenge.size();
challengeOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(challenge.data()));
SecBufferDesc challengeOutBufferDesc;
challengeOutBufferDesc.ulVersion = 0;
challengeOutBufferDesc.cBuffers = 1;
challengeOutBufferDesc.pBuffers = &challengeOutBuffer;
CtxtHandle serverContextHandle;
unsigned long outContextAttributes;
result = AcceptSecurityContext(&serverCredHandle, nullptr, &challengeInBufferDesc, 0, SECURITY_NETWORK_DREP, &serverContextHandle, &challengeOutBufferDesc, &outContextAttributes, &lifetime);
if (result != SEC_I_CONTINUE_NEEDED)
{
return EXIT_FAILURE;
}
challenge.resize(challengeOutBuffer.cbBuffer);
std::cout << "Challenge: [" << challenge << "]\n";
// client challenge response generation
SEC_WINNT_AUTH_IDENTITY_W auth_data;
auth_data.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
auth_data.User = nullptr;
auth_data.UserLength = 0;
auth_data.Domain = nullptr;
auth_data.DomainLength = 0;
auth_data.Password = nullptr;
auth_data.PasswordLength = 0;
CredHandle clientCredHandle;
result = AcquireCredentialsHandleW(nullptr, const_cast<LPWSTR>(WDIGEST_SP_NAME_W), SECPKG_CRED_OUTBOUND, nullptr, &auth_data, nullptr, nullptr, &clientCredHandle, &lifetime);
if (result != SEC_E_OK)
{
return EXIT_FAILURE;
}
SecBuffer challengeResponseInBuffers[4];
// token
challengeResponseInBuffers[0].BufferType = SECBUFFER_TOKEN;
challengeResponseInBuffers[0].cbBuffer = challenge.size();
challengeResponseInBuffers[0].pvBuffer = const_cast<void*>(static_cast<void const*>(challenge.data()));
// method
challengeResponseInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
challengeResponseInBuffers[1].cbBuffer = client_method.size();
challengeResponseInBuffers[1].pvBuffer = const_cast<void*>(static_cast<void const*>(client_method.data()));
// body hash
challengeResponseInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
challengeResponseInBuffers[2].cbBuffer = 0;
challengeResponseInBuffers[2].pvBuffer = nullptr;
// target
challengeResponseInBuffers[3].BufferType = SECBUFFER_STREAM;
challengeResponseInBuffers[3].cbBuffer = client_target.size() * sizeof(wchar_t);
challengeResponseInBuffers[3].pvBuffer = const_cast<void*>(static_cast<void const*>(client_target.data()));
SecBufferDesc challengeResponseInBufferDesc;
challengeResponseInBufferDesc.ulVersion = 0;
challengeResponseInBufferDesc.cBuffers = 4;
challengeResponseInBufferDesc.pBuffers = challengeResponseInBuffers;
std::string challengeResponse;
challengeResponse.resize(package_info->cbMaxToken);
SecBuffer challengeResponseOutBuffer;
challengeResponseOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeResponseOutBuffer.cbBuffer = challengeResponse.size();
challengeResponseOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(challengeResponse.data()));
SecBufferDesc challengeResponseOutBufferDesc;
challengeResponseOutBufferDesc.ulVersion = 0;
challengeResponseOutBufferDesc.cBuffers = 1;
challengeResponseOutBufferDesc.pBuffers = &challengeResponseOutBuffer;
CtxtHandle clientContextHandle;
result = InitializeSecurityContextW(&clientCredHandle, nullptr, const_cast<SEC_WCHAR*>(client_uri.c_str()), 0, 0, SECURITY_NETWORK_DREP, &challengeResponseInBufferDesc, 0, &clientContextHandle, &challengeResponseOutBufferDesc, &outContextAttributes, &lifetime);
if(result != SEC_E_OK)
{
return EXIT_FAILURE;
}
challengeResponse.resize(challengeResponseOutBuffer.cbBuffer);
std::cout << "Challenge Response: [" << challengeResponse << "]\n";
// server verify challenge response
SecBuffer verifyChallengeResponseInBuffers[5];
// token
verifyChallengeResponseInBuffers[0].BufferType = SECBUFFER_TOKEN;
verifyChallengeResponseInBuffers[0].cbBuffer = challengeResponse.size();
verifyChallengeResponseInBuffers[0].pvBuffer = const_cast<void*>(static_cast<void const*>(challengeResponse.data()));
// method
verifyChallengeResponseInBuffers[1].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[1].cbBuffer = client_method.size();
verifyChallengeResponseInBuffers[1].pvBuffer = const_cast<void*>(static_cast<void const*>(client_method.data()));
// uri
verifyChallengeResponseInBuffers[2].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[2].cbBuffer = client_uri_utf8.size();
verifyChallengeResponseInBuffers[2].pvBuffer = const_cast<void*>(static_cast<void const*>(client_uri_utf8.data()));
// body hash
verifyChallengeResponseInBuffers[3].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[3].cbBuffer = 0;
verifyChallengeResponseInBuffers[3].pvBuffer = nullptr;
// realm
verifyChallengeResponseInBuffers[4].BufferType = SECBUFFER_PKG_PARAMS;
verifyChallengeResponseInBuffers[4].cbBuffer = realm.size() * sizeof(wchar_t);
verifyChallengeResponseInBuffers[4].pvBuffer = const_cast<void*>(static_cast<void const*>(realm.c_str()));
SecBufferDesc verifyChallengeResponseInBufferDesc;
verifyChallengeResponseInBufferDesc.ulVersion = 0;
verifyChallengeResponseInBufferDesc.cBuffers = 5;
verifyChallengeResponseInBufferDesc.pBuffers = verifyChallengeResponseInBuffers;
std::string verifyChallengeResponse;
verifyChallengeResponse.resize(package_info->cbMaxToken);
SecBuffer verifyChallengeResponseOutBuffer;
challengeOutBuffer.BufferType = SECBUFFER_TOKEN;
challengeOutBuffer.cbBuffer = verifyChallengeResponse.size();
challengeOutBuffer.pvBuffer = const_cast<void*>(static_cast<void const*>(verifyChallengeResponse.data()));
SecBufferDesc verifyChallengeResponseOutBufferDesc;
verifyChallengeResponseOutBufferDesc.ulVersion = 0;
verifyChallengeResponseOutBufferDesc.cBuffers = 1;
verifyChallengeResponseOutBufferDesc.pBuffers = &verifyChallengeResponseOutBuffer;
result = AcceptSecurityContext(&serverCredHandle, &serverContextHandle, &verifyChallengeResponseInBufferDesc, 0, SECURITY_NETWORK_DREP, &serverContextHandle, &verifyChallengeResponseOutBufferDesc, &outContextAttributes, &lifetime);
if (result != SEC_I_COMPLETE_NEEDED)
{
std::cout << "Challenge Response Verification Failed with [0x" << std::hex << result << "]";
return EXIT_FAILURE;
}
std::cout << "Challenge Response Verified";
return EXIT_SUCCESS;
}
我得到的输出是:
Challenge: [qop="auth",algorithm=MD5-sess,nonce="+Upgraded+v1db70e06e6d35dfd59df6fcd8d3cf7f7671d23e810144d5012972e89ccadaa398f05e8d5ab7a9c0eb3d52b00f4446530312ac45e30f4ac02c",charset=utf-8,realm="realm"]
Challenge Response: [username="",realm="realm",nonce="+Upgraded+v1db70e06e6d35dfd59df6fcd8d3cf7f7671d23e810144d5012972e89ccadaa398f05e8d5ab7a9c0eb3d52b00f4446530312ac45e30f4ac02c",uri="/",cnonce="+Upgraded+v15c1d89757bd55776df6e2788dcdca9220f6aa1af03856d237a0e37a8136b5a44",nc=00000001,algorithm=MD5-sess,response="ee315ddeed6f2d3d68b6cafff9f7d52e",qop="auth",charset=utf-8,hashed-dirs="service-name,channel-binding",service-name="",channel-binding="00000000000000000000000000000000"]
Challenge Response Verification Failed with [0x80090308]
我已经尝试改变挑战/挑战响应和验证部分的输入,但没有实际结果。
MSDN 文档似乎没有多大帮助,我对这篇摘要的大部分见解 API 都来自此处的 .net 参考源: https://referencesource.microsoft.com/#System/net/System/Net/_NTAuthentication.cs
有人知道我做错了什么吗?
更新:
我机器的输出,UseLogonCredential 设置为“1”(虽然我用它测试了“1”和“0”,输出完全相同)对于下面答案中的示例应用程序,我得到以下输出:
Capabilities: 0x800304
wVersion: 0x1
Max Token Size: 0x1000
SERVER: AcquireCredentialsHandle SUCCESS
CLIENT: AcquireCredentialsHandle SUCCESS
CLIENT: InitializeSecurityContext: SEC_I__CONTINUE_NEEDED
SERVER: AcceptSecurityContext: SEC_I__CONTINUE_NEEDED
CLIENT: InitializeSecurityContext: SEC_I_CONTINUE_NEEDED
AcceptSecurityContext SUCCESS
InitializeSecurityContext (2) failed with 0x8009030A
这对我来说似乎可以正常工作,但我不明白为什么您仍然需要最后一个客户端 InitializeSecurityContext,因为服务器端已经验证了它?
我也可以删除第一个 "CLIENT: InitializeSecurityContext",因为它不需要,returns 一个 "blank" 令牌,如果删除它仍然可以正常工作。
我无法开始使用客户端的当前用户凭据,我必须提供 SEC_WINNT_AUTH_IDENTITY 和有效凭据。这有什么原因吗?
默认情况下禁用 wdigest 包,因为它使用不安全的 MD5。 您必须在注册表中启用包才能使用该代码。 KB2871997 and Wdigest – Part 1。为什么要使用 wdigest,因为它不再安全了。
#include <windows.h>
#include <stdio.h>
#define SECURITY_WIN32
#include <sspi.h>
void wmain(int argc, WCHAR *argv[])
{
SECURITY_STATUS ss;
SecPkgInfo *spi = {0};
ss = QuerySecurityPackageInfo(L"wdigest", &spi);
if (ss != SEC_E_OK)
{
wprintf(L"QuerySecurityPackageInfo failed with %u\n", ss);
return;
}
wprintf(L"Capabilities: 0x%X\n", spi->fCapabilities);
wprintf(L"wVersion: 0x%X\n", spi->wVersion);
wprintf(L"Max Token Size: 0x%X\n", spi->cbMaxToken);
LPBYTE lpbOut = NULL;
lpbOut = (LPBYTE)LocalAlloc(LPTR, spi->cbMaxToken);
if (lpbOut == NULL)
{
wprintf(L"LocalAlloc failed with %u\n", GetLastError());
return;
}
CredHandle ServerCred;
CredHandle ClientCred;
TimeStamp LifeTime;
// server side
ss = AcquireCredentialsHandle(NULL, L"wdigest", SECPKG_CRED_INBOUND, NULL, NULL, NULL, NULL, &ServerCred, &LifeTime);
if (ss != SEC_E_OK)
{
wprintf(L"AcquireCredentialsHandle failed with %u\n", ss);
return;
}
else
wprintf(L"SERVER: AcquireCredentialsHandle SUCCESS\n");
// client side
SEC_WINNT_AUTH_IDENTITY AuthIdentity;
WCHAR szDomain[] = L"domain";
WCHAR szPassword[] = L"password";
WCHAR szUser[] = L"user";
AuthIdentity.Domain = (unsigned short *)szDomain;
AuthIdentity.DomainLength = (unsigned long)wcslen(szDomain);
AuthIdentity.Password = (unsigned short *)szPassword;
AuthIdentity.PasswordLength = (unsigned long)wcslen(szPassword);
AuthIdentity.User = (unsigned short *)szUser;
AuthIdentity.UserLength = (unsigned long)wcslen(szUser);
AuthIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
/*
AuthIdentity.Domain = (unsigned short *)NULL;
AuthIdentity.DomainLength = (unsigned long)0;
AuthIdentity.Password = (unsigned short *)NULL;
AuthIdentity.PasswordLength = (unsigned long)0;
AuthIdentity.User = (unsigned short *)NULL;
AuthIdentity.UserLength = (unsigned long)0;
AuthIdentity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;*/
ss = AcquireCredentialsHandle(NULL, L"wdigest", SECPKG_CRED_OUTBOUND, NULL, &AuthIdentity, NULL, NULL, &ClientCred, &LifeTime);
if (ss != SEC_E_OK)
{
wprintf(L"OUTBOUND: AcquireCredentialsHandle failed with 0x%X\n", ss);
return;
}
else
wprintf(L"CLIENT: AcquireCredentialsHandle SUCCESS\n");
SecBufferDesc OutputBuffers;
SecBuffer TempTokensOut[6];
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
CtxtHandle ClientCtxtHandle;
ULONG ClientContextRetFlags = 0;
BOOL bContinue = FALSE;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY | */ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, NULL, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"CLIENT: InitializeSecurityContext: SEC_I__CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext failed with 0x%X\n", ss);
return;
}
}
SecBufferDesc InputBuffers;
SecBuffer TempTokensIn[10];
LPBYTE lpbIn = NULL;
lpbIn = (LPBYTE)LocalAlloc(LPTR, spi->cbMaxToken);
if (lpbIn == NULL)
{
wprintf(L"LocalAlloc failed with %u\n", GetLastError());
return;
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ULONG TargetDataRep = 0;
CtxtHandle ServerCtxtHandle;
ULONG ServerContextRetFlags = 0;
ss = AcceptSecurityContext(&ServerCred, NULL, &InputBuffers, 0 /*ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY*/, TargetDataRep, &ServerCtxtHandle, &OutputBuffers, &ServerContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"SERVER: AcceptSecurityContext: SEC_I__CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"AcceptSecurityContext failed with 0x%X\n", ss);
return;
}
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY | */ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, &InputBuffers, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
wprintf(L"CLIENT: InitializeSecurityContext: SEC_I_CONTINUE_NEEDED\n");
}
else
{
if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext (2) failed with 0x%X\n", ss);
return;
}
}
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = AcceptSecurityContext(&ServerCred, NULL, &InputBuffers, 0 /*ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY*/, TargetDataRep, &ServerCtxtHandle, &OutputBuffers, &ServerContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
}
else if (ss != SEC_E_OK)
{
wprintf(L"AcceptSecurityContext (2) failed with 0x%X\n", ss);
return;
}
else
wprintf(L"AcceptSecurityContext SUCCESS\n");
InputBuffers.ulVersion = SECBUFFER_VERSION;
InputBuffers.cBuffers = 1;
InputBuffers.pBuffers = TempTokensIn;
TempTokensIn[0].BufferType = SECBUFFER_TOKEN;
TempTokensIn[0].cbBuffer = TempTokensOut[0].cbBuffer;
TempTokensIn[0].pvBuffer = TempTokensOut[0].pvBuffer;
OutputBuffers.ulVersion = SECBUFFER_VERSION;
OutputBuffers.cBuffers = 1;
OutputBuffers.pBuffers = TempTokensOut;
TempTokensOut[0].BufferType = SECBUFFER_TOKEN;
TempTokensOut[0].cbBuffer = spi->cbMaxToken;
TempTokensOut[0].pvBuffer = lpbOut;
ss = InitializeSecurityContext(&ClientCred, NULL, L"HTTP//test.com", /*ISC_REQ_INTEGRITY | ISC_REQ_CONFIDENTIALITY |*/ ISC_REQ_REPLAY_DETECT | ISC_REQ_CONNECTION, NULL, SECURITY_NATIVE_DREP, &InputBuffers, NULL,
&ClientCtxtHandle, &OutputBuffers, &ClientContextRetFlags, &LifeTime);
if (ss == SEC_I_CONTINUE_NEEDED)
{
bContinue = TRUE;
}
else if (ss != SEC_E_OK)
{
wprintf(L"InitializeSecurityContext (2) failed with 0x%X\n", ss);
return;
}
else
wprintf(L"InitializeSecurityContext SUCCESS\n");
ss = FreeCredentialsHandle(&ClientCred);
if (ss != SEC_E_OK)
{
wprintf(L"FreeCredentialsHandle failed with %u\n", ss);
return;
}
ss = FreeCredentialsHandle(&ServerCred);
if (ss != SEC_E_OK)
{
wprintf(L"FreeCredentialsHandle failed with %u\n", ss);
return;
}
if (lpbOut != NULL)
LocalFree((HLOCAL)lpbOut);
if (lpbIn != NULL)
LocalFree((HLOCAL)lpbIn);
if (spi != NULL)
FreeContextBuffer((PVOID)spi);
}
当启用 wdigest 包时,这个示例可以工作吗?