Salted Challenge Response Authentication Mechanism (SCRAM) SHA-1






4.94/5 (20投票s)
SCRAM SHA-1 (RFC 5802) 身份验证协议的 C# 实现。
引言
电信通道上安全保障的两个基本要素是保密性和身份验证。保密性确保只有相关方才能访问通信流,而身份验证则试图保证通信通道两端的各方就是他们所声称的那个人。加盐质询-响应身份验证机制 (SCRAM) SHA-1 是 RFC 5802 中定义的标准化身份验证技术。本文简要讨论了此机制,并附带了代码。
这项工作的主要动机是我找不到 SCRAM-SHA1 协议的完整 C# 实现,以用于我目前正在进行的项目,因此我决定自己实现。希望您在需要 C# 实现时不必经历小小的麻烦。
欢迎您 :D
背景
向实体进行身份验证的主要方法是表明您持有某些受限的或秘密的信息,这些信息可以在受到质询时提供。通常,在信息和通信技术 (ICT) 的上下文中,人们会提供用户名和/或密码来向质询实体进行身份验证。有时,身份验证过程必须是双向的,即,A 方向 B 方进行身份验证,B 方也向 A 方进行身份验证。
SCRAM SHA-1 属于提供双向身份验证功能的协议类别。另一个类似的协议是这里讨论的 Socialist Millionaire Protocol (SMP)。在 SCRAM SHA-1 中,与大多数安全身份验证机制一样,密码或秘密信息在协议执行期间永远不会传输。相反,秘密信息加上一个加密安全的随机值被用于在通信通道的两端计算另一个值。传输的就是这个计算出的值。收到传输的值后,通信双方将他们计算出的值与收到的值进行比较。如果这些值匹配,则身份验证成功,否则失败。
SCRAM SHA-1 机制
SCRAM SHA-1 协议假定通信通道的一端是客户端,另一端是服务器。在阅读本文的其余部分时,请牢记这一点。
SCRAM SHA-1 消息序列

SCRAM SHA-1 的执行涉及四条消息的传输和处理;每端(客户端和服务器)各两条。正如上面的图所示,客户端开始过程,发送“客户端首次消息”,并在收到有效格式化的客户端消息后,服务器发送“服务器首次消息”给客户端。客户端处理此消息,如果一切正常,则发送“客户端最后消息”。服务器按预期处理此消息。在此任务结束时,服务器应知道客户端是否已成功身份验证。如果成功,服务器发送“服务器最后消息”,否则发送身份验证失败消息给客户端(或不发送)。收到“服务器最后消息”后,客户端也能够对服务器进行身份验证。
SCRAM SHA-1 加密值
SCRAM SHA-1 协议的四个核心加密值是:
-
客户端随机数(Client nonce):这是客户端随机生成的,最好使用加密随机生成器。此值以及客户端的用户名包含在“客户端首次消息”中。请注意,每次身份验证会话的客户端随机数值必须不同。
-
服务器随机数(Server nonce):“服务器随机数”与客户端随机数类似,包含在“服务器首次消息”中。每次身份验证会话的此值必须不同,并且必须是加密安全的。
-
盐(Salt):盐是由服务器生成的加密安全的随机数。此盐值和密码被输入到一个单向加密函数,该函数生成另一个值。从背景部分可以回想起,此值用于隐藏密码。盐包含在“服务器首次消息”中。
-
迭代次数(Iteration Count):这是服务器生成的数值,表示上述加密函数需要应用于盐和密码的次数才能生成其输出。此迭代次数值在“服务器首次消息”中传输。
注意事项
SCRAM SHA-1 规范**强烈**建议将该协议与提供保密性的另一协议结合使用。换句话说,SCRAM SHA-1 消息应通过加密通道进行交换。其思想是防止窃听者提取传输中的消息内容,然后利用其中的值进行离线字典攻击以提取密码。
对于此处演示,传输层安全性 (TLS) 用于为身份验证协议提供保密性。
使用代码
本节介绍了与已实现的 SCRAM SHA-1 协议相关的例程。还给出了使用规范中给出的测试值(或向量)的身份验证协议的输出。本节最后展示了已实现的协议在 TLS 上运行时客户端和服务器的输出。
已实现的 SCRAM SHA-1 例程
// Client routines
string _user_name = "user";
string _password = "pencil";
string _message = string.Empty;
bool _is_server_authenticated = false;
// Initialize the protocol object
ScramSha1 _client = new ScramSha1(USER_MODE.CLIENT, _user_name, _password);
// Get the client’s first message
_message = _client.GetClientFirstMessage();
// The client sends the message to the server and waits for the server first message
// Assume that the server's first message has been received and is contained in _server_first_message
// Get the client’s final message
_message = _client.GetClientFinalMessage(_server_first_message);
// The client sends this final message to the server and waits for the server's final message
// Assume that the server first message has been received and is contained in _server_final_message
_is_server_authenticated = _client.VerifyServerSignature(_server_final_message);
if (_is_server_authenticated == true)
Console.WriteLine("The server is authenticated \n");
else
{
Console.WriteLine("The server is not authenticated \n");
// Send authentication failure message if it is the policy
}
此协议实现可以以两种模式之一运行:服务器或客户端。要作为客户端开始身份验证过程,请调用类构造函数,即 ScramSha1(USER_MODE, string, string)
。构造函数接收用户模式、用户名和密码参数,如上面的代码片段所示。通过调用 GetClientFirstMessage()
例程可获取“客户端首次消息”。此消息发送给服务器。一旦收到“服务器首次消息”,客户端就将其传递给 GetClientFinalMessage(string)
例程。此例程返回“客户端最后消息”,该消息将发送给服务器。最后,将收到的“服务器最后消息”传递给 VerifyServerSignature(string)
例程。此例程返回的布尔值指示服务器是否已成功身份验证。
该实现将客户端随机数的默认长度设置为 32 字节。为了增加离线字典攻击的难度,客户端可能希望增加此随机数的长度。这可以通过在客户端模式下使用构造函数 ScramSha1(USER_MODE, string, string, int)
来实例化类,并传递所需的客户端随机数长度来实现。
如果在处理收到的服务器消息期间发生错误,GetClientFirstMessage()
和 GetClientFinalMessage(string)
例程将返回一个空字符串。这些错误可能是由于服务器消息格式不正确造成的。
// Server routines
string _user_name = string.empty;
string _password = string.empty;
string _message = string.Empty;
bool _is_client_authenticated = false;
// Assume that the server has just received the first message of the client and it is contained on _client_first_message
// Initialize the protocol object
ScramSha1 _server = new ScramSha1(USER_MODE.SERVER, _client_first_message);
// Get the user name
_user_name = _server.GetUserName();
// Use the _user_name to get the associated password from the authentication database and set this value in _password.
// Get the server’s first message
_message = _server.GetServerFirstMessage(_password);
// The server sends the message to the client and waits for the client's final message
// Assume that the client's final message has been received and is contained in _client_final_message
// Get the server’s final message
_message = _server.GetServerFinalMessage(_client_final_message, ref _is_client_authenticated);
if (_is_client_authenticated == true)
{
Console.WriteLine("The client is authenticated \n");
//Send the server final message
}
else
{
Console.WriteLine("The client is not authenticated \n");
// Send authentication failure message if it is the policy
}
以服务器模式执行协议遵循上面代码片段所示的模式。请注意,在这种情况下使用的构造函数与在客户端模式下使用的构造函数不同。只有在收到“客户端首次消息”后才能实例化服务器模式。收到的客户端消息被传递给此构造函数:ScramSha1(USER_MODE, string)
。服务器可以通过调用 GetUserName()
例程来获取客户端的用户名。服务器使用此用户名从身份验证数据库中获取密码。如果存在,则将密码传递给 GetServerFirstMessage(string)
例程。此例程返回一个包含“服务器首次消息”的字符串,该字符串将发送给客户端。当服务器收到“客户端最后消息”时,它将其传递给 GetServerFinalMessage(string ,ref bool)
例程。此例程还通过引用传递一个布尔变量。如果布尔变量值为 true
,则客户端已身份验证,此例程返回的字符串将作为上面代码片段所示发送给客户端。否则,可以发送身份验证失败消息。
服务器随机数的长度以及盐的长度可以通过在服务器模式下调用 ScramSha1(USER_MODE, string, int, int)
构造函数来设置。增加客户端随机数的理由同样适用于服务器随机数和盐。
当处理收到的客户端消息时发生错误,GetServerFirstMessage(string)
例程返回一个空字符串。另一方面,当发生错误或客户端未被身份验证时,GetServerFinalMessage(string ,ref bool)
例程会返回一个空字符串。
在服务器模式下,可以通过调用 SetIterationCountLimits(int, int)
例程来设置迭代次数的下限和上限。默认的下限和上限分别为 4000 和 5000。重要的是这些限制不要设置得太高或太低。设置得太低会增加对该身份验证机制进行离线字典攻击成功的几率。如果设置得太高,可能会使在处理器受限的设备上运行的客户端不堪重负。为了防止恶意服务器降低客户端设备的性能,客户端可以通过将此值传递给 SetMaximumIterationCount(int)
例程来设置其可以容纳的最大迭代次数。此客户端最大值的默认值为 10000。如果从服务器收到的迭代次数高于此最大值,则身份验证过程失败。
使用测试向量执行 SCRAM SHA-1
SCRAM SHA-1 协议 RFC 提供了用于测试实现是否符合身份验证规范的值。使用测试向量运行此实现的需要三样东西:调用类的静态例程 UseTestVectors(bool)
,并将参数 true
传递给它以将实例设置为测试模式。其次,用户名设置为“user”,最后,密码设置为“pencil”。要查看测试模式下身份验证协议的输出,请运行附带的控制台应用程序实例,键入字母“T”然后按 Enter 键。屏幕应显示与下图类似的输出。

使用 TLS 执行 SCRAM SHA-1
此处使用的 TLS 实现故意做得不够优雅,以便足够简单地演示在 TLS 上使用身份验证协议。
服务器的 TLS 实现基于 Kerry Jiang 的 SuperSocket 类。服务器的 SCRAM SHA-1 逻辑可以在 ScramCommand
类中找到。客户端的逻辑包含在 SCRAMSha1TestClient
类中。
首先,应运行应用程序的一个实例。键入字母“S”然后按 Enter 键以服务器模式启动它。然后运行另一个实例,键入字母“C”然后按 Enter 键以客户端模式启动它。如果一切按计划进行,您应该会看到下面的屏幕截图。


要强制身份验证过程失败,可以为客户端和服务器设置不同的密码,然后观察输出。
历史
17/12/2013:第一个版本
2014/03/26:使客户端和服务器的 TLS 机制更加健壮。