天天看點

WebRTC STUN | Short-term 消息認證

作者:泰一

來源:

碼神說公衆号 本文是 STUN 協定系列第 1 篇

  • 導讀與 STUN 概覽
  • 先談 HMAC
  • 再看 Short-term Credential Mechanism
  • 計算 Message Integrity
    • HMAC 輸入
    • HMAC key
    • 計算過程
  • 源碼剖析
    • ValidateMessageIntegrityOfType 函數
    • AddMessageIntegrityOfType 函數
  • 抓包分析
  • 參考

Session Traversal Utilities  for NAT (STUN) 是一個

client/server

協定,支援兩種類型的事務,分别是

request/response

事務和

indication

事務。

STUN 本身并不是一種 NAT 穿越的解決方案,它是協定,作為一個工具或者内部元件,被 NAT 穿越的解決方案(比如 ICE 和 TURN)所使用。

STUN 協定能夠幫助處于内網的終端确定 NAT 為其配置設定的外網 IP 位址和端口(通過

XOR-MAPPED-ADDRESS

屬性),還可以用于 NAT 綁定的保活(通過

binding indication

消息)。

在 ICE 中,STUN 協定用于連通性檢查和 ICE 的保活(通過

binding request/response

),在 TURN 協定中,STUN 協定用于

Allocation

的建立,可以作為中繼資料的載體(比如

sendindication

dataindication

)。也就是說,ICE 和 TURN 是兩種不同的 STUN Usage。

正因為 STUN 協定是其他協定或者 NAT 解決方案的基礎,是以掌握 STUN 協定是非常關鍵的。

本文作為 STUN 協定系列的第一篇,将介紹 STUN 協定的 short-term 消息認證機制,并緻力于講清兩個點:一個是究竟取 STUN 消息的哪一部分内容參與 HMAC-SHA1 的計算,另一個是

request/response

消息究竟使用哪一方的 password 作為 HMAC key 去計算 message integrity。

Let's Go!

HMAC,Keyed-Hashing for Message Authentication Code[1] 是一種基于加密哈希函數的消息認證機制,所能提供的消息認證包括兩方面:

  1. 消息完整性認證,能夠證明消息内容在傳送過程中沒有被修改。
  2. 信源身份認證,因為通信雙方共享了認證的密鑰,是以接收方能夠認證消息确實是發送方所發。

HMAC 運算利用雜湊演算法,以一個消息 M 和一個密鑰 K 作為輸入,生成一個定長的消息摘要作為輸出。HMAC 的一個典型應用是用在

挑戰/響應(Challenge/Response)

身份認證中,認證流程這裡不做介紹。

短期證書機制 short-term credential mechanism[2] 是一種對 STUN 消息進行完整性保護與認證的機制。使用短期證書機制的前提是:在 STUN 消息傳輸之前,用戶端和服務端已經通過其他協定交換了彼此的證書。比如在 ICE 的應用中,用戶端和服務端會通過單獨的信令通道來交換彼此的證書,證書在媒體會話期間适用。

證書由 username 和 password 組成,因為是短期證書,是以具有時效性,可以天然的降低重播攻擊的風險。證書用于對 STUN 請求與響應消息的完整性檢查,而具體的實作機制就是 HMAC,計算出的 HMAC 結果存儲在 STUN 的

MESSAGE-INTEGRITY

屬性中。

STUN 的

MESSAGE-INTEGRITY

屬性包含了對 STUN 消息進行 HMAC-SHA1 計算之後的 HMAC 值,由于使用 SHA-1 哈希函數,是以計算出來的 HMAC 值固定為 20 位元組。在後面的介紹中,我會使用縮寫

M-I

來表示 Message Integrity,并将對 STUN 消息進行 HMAC-SHA1 計算後得到的 HMAC 值稱為 M-I 值。

那麼在 short-term 機制下,M-I 值是怎樣計算的呢?答案是:以 request 消息的發起方的視角為基準,STUN 消息的一部分作為 HMAC 算法的輸入,對端的 password 作為 HMAC 算法的 key。

不過,是要用 STUN 消息的哪一部分作為輸入呢?RFC8489: MESSAGE-INTEGRITY[3] 中給出了答案,但是乍一讀,很多人可能會暈掉,是以接下來我會為大家更好的去解釋這一段描述。

關于 M-I 值的計算,分為兩個大的方向,一個是作為 STUN 消息的發送方,需要在構造 STUN 消息時同時構造 M-I 屬性,而構造 M-I 屬性,就必然要計算 M-I 值;另一個是作為 STUN 消息的接收方,需要在收到 STUN 消息後驗證其 M-I 屬性,具體的做法就是比較 M-I 屬性的 M-I 值是否和接收方計算出的 M-I 值一緻,是以也是要計算 M-I 值。

無論是構造 M-I 屬性時計算 M-I 值還是驗證 M-I 屬性時計算 M-I 值,流程都是完全一樣的,隻需要了解好三個點:

  1. STUN 消息的 M-I 屬性之前的(不包括 M-I),包括頭部在内的所有内容作為 HMAC 的輸入資料。
  2. STUN 消息的 M-I 屬性之前的(不包括 M-I),包括頭部在内的所有内容的長度作為 HMAC 的輸入長度。
  3. 在 HMAC 計算之前,要調整 STUN 頭部字段

    message length

    的值,

    message length

    的大小為 M-I 屬性之前的(包括 M-I)總的長度。

關于第 3 點,需要注意的是,在構造 M-I 屬性時是不需要調整

message length

值的,一般是在驗證 M-I 屬性時調整

message length

值。這是因為,對于接收方收到的 STUN 消息,可能在 M-I 屬性之後還存在

FINGERPRINT

或者

MESSAGE-INTEGRITY-SHA256

屬性,是以

message length

需要去掉這兩種屬性的長度。

然而,對于發送方,在構造 STUN 消息的 M-I 屬性時,還未構造

FINGERPRINT

MESSAGE-INTEGRITY-SHA256

message length

不需要做調整。在下文的源碼剖析部分,我們會深刻的了解以上幾點,在進入源碼剖析之前,還需要再介紹一下作為 HMAC key 的 password 是如何運用的。

在 short-term 機制下, 對于 request 發起方,HMAC 的 key 使用的是對方的 password,即 SDP 中的 ice-pwd 描述。

remark: 上文中提到 short-term 證書是由 username 和 password 組成,但是實際上 short-term 隻用到了 password,并未用到 username。

remark: username 的規則是:對方的 ufrag: 自己的 ufrag。

舉個例子,taiyi 釋出自己的流到 SFU。taiyi 和 SFU 的名字與密碼資訊如下:

taiyi: ufrag = sLop passwd = GCR3LqC+baeBQ7NxdWb8Q4Oc

SFU: ufrag = N+vv passwd = da2vlP6ZJrd4VbnSEP/AdjcW

taiyi 發送 STUN BindingRequest 消息給 SFU:

  1. username:

    N+vv:sLop

  2. short-term 使用的 HMAC key 應該是 SFU 的 password:

    da2vlP6ZJrd4VbnSEP/AdjcW

SFU 收到來自 taiyi 的 BindingRequest 後,就可以使用自己的 password 計算消息的 M-I 值,以進行消息認證。認證成功後,SFU 回複 BindingResponse 給 taiyi:

  1. N+vv:sLop

  2. short-term 使用的 HMAC key  應該是 SFU 的 password:

    da2vlP6ZJrd4VbnSEP/AdjcW

可以知道,在 taiyi 與 SFU 的這一次 STUN binding request/response 事務中,response 的 username 規則以及使用的 password 與 request 完全一緻。

可以記為:username 與 password 的規則都是以 request 的發起方作為基準,response 向 request 看齊。

同理,SFU 發送 STUN BindingRequest 消息給 taiyi,taiyi 回複 BindingResponse,此時以 request 發起方 SFU 為準:

  1. sLop:N+vv

  2. short-term 使用的 HMAC key  應該是 taiyi 的 password:

    GCR3LqC+baeBQ7NxdWb8Q4Oc

為了能夠更深刻的了解上述流程,我畫了一張圖,如下:

WebRTC STUN | Short-term 消息認證
note: 上圖所寫的 ufrag 和 password 并非 rfc 規定的标準的格式,僅為了更好的了解。

下面介紹對 STUN 消息進行完整性驗證時的 M-I 值的計算過程。假設 SFU 收到的 STUN binding request 消息如下:

// 20 bytes
[ STUN HEADER ]
// 12 bytes(2 bytes type, 2 bytes length, 8 bytes username)
[ USERNAME ]
// 24 bytes (2 bytes type, 2 bytes length, 20 bytes hmac-sha1)
[ MESSAGE-INTEGRITY-ATTRIBUTE ]
// 8 bytes(2 bytes type, 2 bytes length, 4 bytes crc32 value)
[ FINGERPRINT ]      

計算流程對應的僞代碼如下:

// 去掉 8 位元組大小的 Fingerprint 屬性,
// 然後将消息序列化為位元組,得到 stun_binary,
// 注意,不要去掉 MessageIntegrity 屬性。
stun_msg = (header, 
  attributes[Username, MessageIntegrity, 
    Fingerprint])
// 将序列化後的消息去掉最後 24 位元組的 M-I 屬性,
// 得到更新後的 stun_binary。
stun_binary = 
  stun_msg.remove(Fingerprint).marshal_binary()
stun_binary = 
  stun_binary[0 : len(stun_binary) - 24]
// 生成 HMAC key。
key = password
// 計算 HMAC,得到 20 位元組的 M-I 值。
h = hmac.new(hash.sha1, key);
h.update(stun_binary);
mi = h.Sum(null);
// 比較 mi 是否和消息攜帶的 M-I 值一緻。
memcmp(
  stun_msg.attributes.MessageIntegrity.value, 
  mi, 20)      

參考 WebRTC M88。

STUN 的 short-term 消息認證主要包括:構造 M-I 屬性和驗證 M-I 值。相關的類和函數如下:

class StunMessage {
  // Validates that a raw STUN message 
  // has a correct MESSAGE-INTEGRITY value.
  static bool ValidateMessageIntegrity(
    const char* data, size_t size, 
    const std::string& password);
  // Adds a MESSAGE-INTEGRITY attribute 
  // that is valid for the current message.
  bool AddMessageIntegrity(
    const std::string& password);
};      

該函數用于檢驗所收到的 STUN 消息的完整性,對消息的來源進行認證。可以結合上文

HMAC 輸入

這一節中提到的 3 點來了解該函數驗證 STUN 消息完整性的流程。

首先,驗證消息的大小:

  1. STUN 消息頭部大小固定為

    kStunHeaderSize = 20

    位元組。
  2. STUN 消息的屬性是 4 位元組對齊的。
if ((size % 4) != 0 || 
  size < kStunHeaderSize) {
  return false;
}      

是以,消息的長度不能小于 20 且要是 4 的倍數。

接着,從 STUN 消息的頭部擷取字段

message length

的值。

uint16_t msg_length = rtc::GetBE16(&data[2]);
if (size != (msg_length + kStunHeaderSize)) {
  return false;
}      

message length

字段表示 STUN 消息的屬性的長度,不包括 20 位元組的 STUN 消息頭部。是以,STUN 消息的大小

size = msg_length + kStunHeaderSize

接着,尋找 STUN 消息的 M-I 屬性,定位其在整個消息中的位置

mi_pos

。在周遊尋找 M-I 屬性的過程中,如果目前屬性不是 M-I 屬性,那麼就需要跳到下一個屬性,如果沒有找到 M-I 屬性,則傳回 false,表示消息完整性校驗失敗。因為 STUN 消息的屬性是按照 4 位元組對齊,是以在計算

current_pos

的時候可能需要加上填充位元組的長度。

比如,目前 STUN 消息的屬性是

USERNAME

屬性,屬性長度為 5 位元組,那麼會有 3 位元組的值為 0x00 的 padding 填充,進而保證 STUN 屬性的 4 位元組對齊的原則,此時

current_pos

需要再加上 3。
size_t current_pos = kStunHeaderSize;
bool has_message_integrity_attr = false;
while (current_pos + 4 <= size) {
  uint16_t attr_type, attr_length;
  // Getting attribute type and length.
  attr_type = 
    rtc::GetBE16(&data[current_pos]);
  attr_length = rtc::GetBE16(
    &data[current_pos + sizeof(attr_type)]);
  // If M-I, sanity check it, and break out.
  if (attr_type == mi_attr_type) {
    if (attr_length != mi_attr_size ||
      current_pos + sizeof(attr_type) + 
      sizeof(attr_length) + attr_length > size) 
    {
        return false;
    }
    has_message_integrity_attr = true;
    break;
  }
  // Otherwise, skip to the next attribute.
  current_pos += sizeof(attr_type) + 
    sizeof(attr_length) + attr_length;
  if ((attr_length % 4) != 0) {
    current_pos += (4 - (attr_length % 4));
  }
}      

在找到 M-I 屬性,并記錄其在消息中的位置

mi_pos

之後,開始計算這個 STUN 消息的 M-I 值,用于和這個消息中自帶的 M-I 值進行比較。

首先需要判斷 STUN 消息的 M-I 屬性的後面是否還有其他屬性,比如

FINGERPRINT

。如果有,那麼需要調整 STUN 頭部字段

message length

的值,具體的做法就是減去 M-I 屬性之後的所有屬性的總長度。

size_t mi_pos = current_pos;
std::unique_ptr<char[]> 
  temp_data(new char[current_pos]);
memcpy(temp_data.get(), data, current_pos);
if (size > mi_pos + 
  kStunAttributeHeaderSize + mi_attr_size) 
{
  // Stun message has other attributes
  // after message integrity.
  // Adjust the length parameter in stun 
  // message to calculate HMAC.
  size_t extra_offset = size - 
    (mi_pos + kStunAttributeHeaderSize 
    + mi_attr_size);
  size_t new_adjusted_len = 
    size - extra_offset - kStunHeaderSize;
  // Writing new length of the STUN 
  // message @ Message Length in temp buffer.
  rtc::SetBE16(temp_data.get() + 2, 
    static_cast<uint16_t>(new_adjusted_len));
}      

在将調整後的

message length

的值設定到

temp_data

之後,開始計算 HMAC-SHA1 值,計算過程可參考 rfc2104。

char hmac[kStunMessageIntegritySize];
size_t ret = rtc::ComputeHmac(rtc::DIGEST_SHA_1, 
  password.c_str(), password.size(),
  temp_data.get(), mi_pos, hmac, sizeof(hmac));      
remark:

temp_data

mi_pos

分别是參與 HMAC-SHA1 計算的消息内容與長度(不包括 M-I 屬性)。

最後,比較計算得到的 M-I 值是否和 STUN 消息中 M-I 屬性中的 M-I 值一緻。

return memcmp(
  data + current_pos + kStunAttributeHeaderSize, 
  hmac, mi_attr_size) == 0;      

該函數用于在發送 STUN 消息之前構造其 M-I 屬性。

首先,增加僞值為 0 的 M-I 屬性。

auto msg_integrity_attr_ptr = 
  std::make_unique<StunByteStringAttribute>(
    attr_type, std::string(attr_size, '0'));
auto* msg_integrity_attr = 
  msg_integrity_attr_ptr.get();
AddAttribute(std::move(msg_integrity_attr_ptr));      

接着,計算 STUN 消息的 HMAC 值:

  1. 将消息序列化為位元組。
  2. 計算參與 HMAC 計算的消息内容長度

    msg_len_for_hmac

    ,為消息總長度減去最後 24 位元組的 M-I 屬性的長度。
  3. ComputeHmac

    函數計算 M-I 值。
ByteBufferWriter buf;
if (!Write(&buf))
  return false;
int msg_len_for_hmac = static_cast<int>(
  buf.Length() - 
  kStunAttributeHeaderSize - 
  msg_integrity_attr->length());
  
char hmac[kStunMessageIntegritySize];
size_t ret = rtc::ComputeHmac(
  rtc::DIGEST_SHA_1, key, keylen, 
  buf.Data(), msg_len_for_hmac, 
  hmac, sizeof(hmac));      

remark: 計算 HMAC 時的輸入内容取 M-I 屬性之前的内容,不包括 M-I 本身。

remark: 此時消息還沒有增加

FINGERPRINT

等 M-I 之後的屬性,是以消息頭部的

message length

字段不需要調整。

最後,将計算好的 M-I 值替換掉之前的僞值。

msg_integrity_attr->CopyBytes(hmac, attr_size);      

使用 wireshark 抓取 STUN binding request 消息,如下:

WebRTC STUN | Short-term 消息認證

結合上圖,可以直覺的看到參與 HMAC 計算的内容為 M-I 屬性上方的部分,不過在計算前要調整

message length

字段值,減去 8 位元組的

FINGERPRINT

屬性。

對應的 STUN binding response 消息如下:

WebRTC STUN | Short-term 消息認證

觀察 response 消息的 username,和 request 的 username 一緻。另外,request 和 response 使用 HMAC-SHA1 計算 M-I 值所使用的 key 都是一樣的,全部使用 responser 的 password(以 requester 為基準,對方的 password 作為 key)。

不過雙方的 password 并不會出現在 STUN 消息中,一般是在 STUN 消息傳輸前通過單獨的信令通道共享彼此的 password。

最後,我們發現在兩個消息的

USERNAME

屬性中,都有 3 位元組的填充,值為 0x00。填充位元組不算入

USERNAME

屬性的長度。

下一篇,将會介紹 STUN 協定的資料包的格式以及如何與其他協定(

DTLS/RTP/RTCP

)的資料包進行區分。感謝閱讀。

[1]

HMAC:

https://tools.ietf.org/html/rfc2104

[2]

Session Traversal Utilities for NAT (STUN):

https://tools.ietf.org/html/rfc8489?#section-9.1

[3]

RFC8489: MESSAGE-INTEGRITY:

https://tools.ietf.org/html/rfc8489#section-14.5
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
WebRTC STUN | Short-term 消息認證

繼續閱讀