Noise Protocol简介

最近在研究 VPN 相关的技术,会做个简单的总结,稍微系统地去介绍安全协议及其系统

0x00 何为Noise Protocol

官网的概述,其定义为:

Noise is a framework for building crypto protocols. Noise protocols support mutual and optional authentication, identity hiding, forward secrecy, zero round-trip encryption, and other advanced features.

太过含糊,仅仅强调其功能特性; 不过从作者在Schedule 34th Chaos Communication Congress的介绍,更具象一些,说明如下:

Noise is a framework that helps in creating secure channel protocols

所谓的secure channel,就是安全通道,比如TLS, IPsec, SSH都是; 也就是noise protocol就是用于快速方便去构建安全信道的一个框架。

0x01 WHY

一个自然而然的问题是,为什么需要这样的框架? 我个人的理解包括:

  • 从安全的角度来看,虽然我们拥有很多基础的密码原语支持,比如公钥体系,签名体系,对称加密,HASH 等, 但是在业务落地的时候这些东西如何组合也是个问题和风险点。如果组织不合理,非常有可能造成进一步的安全漏洞。基于此,形式化和清晰化的去描述和定义一个”安全协议”, 可以从更系统的层面去分析其安全问题,更全面的评估其可靠性,也避免了一些『自定义』方式带来的安全漏洞。 这个方面其实有点像我们开放我们的『加密算法』,可以让第三方去审计/评估,但是不影响加密算法的可用性。noise protocol同样也是,其明确定义了不同模式的交互方式和协议,我们可以更清晰的审计其健壮性

  • 从应用角度而言, two-parties的安全信道构建一直是有强烈诉求的, 但是不同的业务场景对于安全的要求也不尽相同,比如是否需要验证身份等, 所以去构建一个『框架』,让用户可以根据自己的业务场景方便构建自己安全信道的构建方式,是能满足更广大用户

0x02 Noise 的抽象和规范

介绍完了 Noise 的简单背景之后,我们来分析 Noise 是如何去解决这个问题的。

2.1 抽象

如下分析来自[1]的 PPT

一个安全信道的构建,可以划分为如下几个阶段 :

  1. 握手(Handshake)阶段,这阶段主要用于协议的协商和AKE(Authenticated Key Exchange) 相关事宜的完成,即包括:

    • Negotiation : 协议协商等
    • AKE : 身份验证&密钥交换
  2. 数据传输(Transport phase)阶段,根据握手阶段生成的对称密钥,加密传输相关数据

如下图所示 :

Secure Channel

但是从实践应用的角度,为了保障安全性(比如考虑前置安全性), 我们会根据情况,重新协商密钥(AKE), 所以整个交互过程,会变成如下模式:

[01] Negotiation + N (AKE + n * transport )

意味着:

  1. 一般就需要一次协商阶段/或者不需要
  2. 会有多次(AKE + trans)阶段,来保障传输的安全性

Noise protocol就是更关注后者的实现, 如下:

secure_channel_of_noise

如此一来,一个包含Noise Protocol的应用,其安全信道的组织方式大概如下:

noise_framework_overview

这个也是Wireguard的组织方式,后续的文章会再详细介绍

2.2 规范

有了上面的抽象之后,在具体实现上面, Noise 也做了相关的规范和约定;主要包括两个方面,其一是模式的约定,这里面主要说明了根据不同的业务场景(比如是否需要身份验证等),如何进行握手协议; 其二是密码算法的约定,主要约定了交互时候具体选择的密码协议等

2.2.1 模式约定

重点关注基础(fundamental),交互式(Interactive)的沟通模式

在一个交互式的场景下,比如Alice和Bob,我们假设他们需要完成握手协议,以便后续的数据传输。一个问题是,Alice 和 Bob 有多少种握手沟通方式? 这里, 我们其用两个字母表示,分别表示发送者(Alice)和 Bob(响应者)对于对方static key的诉求情况 :

对于Alice(请求初始化者)有如下四个情况 :

字母 spec说明 备注
N No static key for initiator 无需发送者的静态key(对发送者不进行身份验证)
K Static key for initiator Known to responder 已知(通过其他旁路方式)发送者的静态key, 可以进行身份验证
X Static key for initiator Xmitted (“transmitted”) to responder 通过握手传输交互发送者的静态key
I Static key for initiator Immediately transmitted to responder, despite reduced or absent identity hiding 通过握手传输,且离开发送静态 key, 这可能使其身份更容易暴露

对于Bob(请求响应者)有如下三个情况:

字母 spec说明 备注
N No static key for responder 无需响应者的静态key
K Static key for responder Known to initiator 已知响应者的静态key
X Static key for responder Xmitted (“transmitted”) to initiator 通过握手传输交互静态key

基于如上的自由组合,一共会有12种不同的模式; 再加上如下的规则 :

The fundamental handshake patterns perform DH operations for authentication (“es” and “se”) as early as possible

给定任意XY, 原则上面,我们都能推导其交互过程,比如XX,其具体如下:

A status B status 备注
s(x),e(x),rs(x),re(x) s(x),e(x),rs(x),re(x) 初始状态
s(x),e(y),rs(x),re(x) –> e s(x),e(x),rs(x),re(x) 生成e, 发送
s(x),e(y),rs(x),re(x) s(y),e(y),rs(x),re(y) B 知道e,s,re,起可以发送的 DH 有ee, es
s(x),e(y),rs(x),re(x) <–e,ee,s,es s(y),e(y),rs(x),re(y)
s(y),e(y),rs(y),re(y) s(y),e(y),rs(x),re(y) 发送 A.s, 其可以DH 有se,ss
s(y),e(y),rs(y),re(y) –> s,se s(y),e(y),rs(y),re(y)

2.2.2 基础算法约定

我们知道,密码学虽然定义了其有限的基本的『能力原语』,但是实现上面却各不相同,纷繁杂乱;一个 hash 方法,就有大几十种选择,不同的实现可能存在着或多或少的安全风险。为此,Noise框架直接规范了其依赖的函数的具体算法,对于这些规范内的函数,其安全性和可控性是经过严格审计的,具体如下:

类型 可选项 说明
密钥交互 DH25519 DHLEN = 32
密钥交互 DH448 DHLEN = 56
加密函数 ChaChaPoly 16 byte authentication data
加密函数 AESGCM 16 byte authentication data
哈希函数 SHA256 HASHLEN = 32, BLOCKLEN = 64
哈希函数 SHA512 HASHLEN = 64, BLOCKLEN = 128
哈希函数 BLAKE2s HASHLEN = 32, BLOCKLEN = 64
哈希函数 BLAKE2b HASHLEN = 64, BLOCKLEN = 128

如上,我们就可以约定Noise Protocol Name, 也可以通过其名字可以知道其协议过程,比如从Noise_NX_25519_AESGCM_SHA256, 可以得到:

  • NX = Pattern name
  • 25519 = DH name
  • AESGCM = Cipher name
  • SHA256 = Hash name

同时根据具体选择的算法,我们还可以估算一个Noise Protocol 交互时候其数据包的大小, 比如Noise_XX_25519_ChaChaPoly_SHA256模式,其不同的数据包大小如下:

1
2
3
4
5
6
7
8
9
XX:
-> e ## 1
<- e, ee, s, es ## 2
-> s, se ## 3
# 说明:
## 1. DH_25519的公钥为32字节,明文, 总长度为32
## 2. e为32字节, s为48字节(AEAD模式会增加16字节),payload(为空)加密后再增加16字节, 一共96字节
## 3. s加密后为48字节,加上payload为空的加密串,一共64字节

0x03 Noise Protocol 实践分析

为了助于我们对noise protocol加密协议有进一步的理解,对于给定的一个Noise Protocol Name, 我们要分析其包体格式, 比如上面的Noise_XX_25519_ChaChaPoly_SHA256, 其交互流程如下:

1
2
3
4
XX:
-> e
<- e, ee, s, es
-> s, se

那么在每个阶段,其具体发送的内容是什么呢?

其实在实现的时候, 发送方都维护了一个类似于状态机的内容,而e,s,ee之类的,则类似于action, 用于更新相关状态的内容;所以包的格式其实可以理解为就是对于action的一系列响应的结果;下面具体说明下:

首先是基础函数方面, 有如下几个能力 :

  • $DH(ee|es|se|ss)$ : 用于计算一个共享密钥, 例子用到的是DH25519
  • $hash(data)$ : 用于计算一个哈希值, 我们这边用的是SHA256
  • $encrypt(k,h,data)$ : AEAD 模式的加密函数
  • $HKDF(ck,data)$ : KDF 函数,用于生成新的key chain

其次是状态机方面,对于 Alice/Bob,其本地都维护了如下的信息:

关键字 说明
s,e 本地的临时/静态 DH组合对
rs,re 对方的临时/静态的 DH 组合对
h 当前的 hash 值,初始为空
ck chain key, 初始为空,用于通过 KDF 生成新key
k, n 加密k, 加密时候会通过(k,n,hash) 和 AEAD 模式进行对称加密
msg 当前要发送的消息内容
payload 当前要携带的额外数据

最后是 action 方面, 对于不同的 action,其定义如下:

  • action e :

    • $msg += e$
    • $h = hash(h||e)$
  • action s :

    • $s = k.empty() ? s : encrypt(s,k,h)$
    • $msg+=s$
    • $h = hash(h||s)$
  • action ee/es/ss/se :

    • $v = DH(ee|es|se|ss)$
    • $ck,k = HKDF(ck,v)$
    • $n = 0$
  • action token end(全部处理完 token) :

    • $payload = encrypt(payload,k,h) (if k!=0 )$

最后发送msg+payload

有了如上的说明,我们再详细看看Noise_XX_25519_ChaChaPoly_SHA256的交互情况 :

  1. -->e : 按照上面的分析, 会发送明文的e, 同时由于其不生成k, 因为也不会额外计算payload, 其发送包体只有32字节
  1. <--e,ee,s,es : 其有4个 token,拆分分析, 如下表格:
action msg h k ck
e e h1=hash(e) 0 0
ee e h1 k1 ck1,k1 = HKDF(ck,ee)
s e,enc(s) h2=hash(h1+enc(s)) k1 ck1
es e,enc(s) h2 k2 ck2,k2=HKDF(ck1,es)
end e,enc(s),enc(null)

具体格式如下:

noise_xx_demo

最后一个命令同理,不再赘述;

基于上面的分析,详细大家可以直接计算msg的包体大小,也就是上一节里面展示的;

如下是Wireshark抓包的示意图:

noise_wireshark_demo

具体实现代码方面,可以参考noiseprotocol:Noise Protocol Framework - Python 3 implementation

可以参看其README里面关于server/client的编写,实现一个快速简单的原型;

参考文档