TLS协议的主要目标是在两个通信应用之间提供私密性和数据完整性。这个协议由两层组成:TLS记录协议和TLS握手协议。最低层是基于一些可靠传输协议(如TCP)的TLS记录协议。TLS记录协议提供的连接安全有两个基本性质:
TLS的一个优点是它是应用层协议无关的。高层协议可以透明地置于TLS协议之上。然而,TLS标准不会指定协议怎样为TLS增加安全性。关于怎样初始化TLS握手、怎样理解认证证书交换的相关决策,则留给运行于TLS之上的协议的设计者和实现者做判断。
略。
本文是TLS 1.1协议的一个修订,修订内容包括提升了灵活性,尤其是密码学算法的协商。主要变更如下:
TLS协议的目标按照优先级排列如下:
本文和TLS协议自身都是基于由Netscape公布的SSL 3.0规范。TLS和SSL3.0的差异并不是引人注目的,但差异也足够大到使各种版本的TLS和SSL3.0不能互操作(虽然每种协议都包含一个机制使其能够兼容之前的版本)。本文的主要目标读者是协议实现者和对协议进行密码分析的人员。这个思想贯穿于这篇规范的制定过程,并致力于反应这两部分群体(译者注:协议实现和协议分析人员)的需求。出于这个原因,很多算法相关的数据结构和规则被包含在文档的内容中(或附录中),以方便获取。
本文并不致力于提供任何关于服务定义和接口定义的细节,虽然处于维护安全的坚实性,它确实有选择地覆盖了一些策略的区域。
本文使用另外的表示方法处理数据格式。下面会用到非常基础甚至是有些随便定义的陈述语法。这种语法源自多处。虽然它在语法上像编程语言C,在语法和目的上像XDR(External Data Representation),但有太多相似之处是有风险的。这种陈述语言只用于将TLS文档化,在这个特定目标外该语言不会有普遍的应用。
所有数据条目的描述都是被显示指定的。基本数据块大小是1个字节(即8位)。多个字节数据条目是字节的串联,从左到右,从上到下。从字节流的角度 看,一个多字节条目(在例子中是一个数值)的组织方式(使用C的记法)如下:
value = (byte[0] << 8*(n-1)) | (byte[1] << 8*(n-2)) | ... | byte[n-1];
对于多字节数值这个字节序是普通的网络字节序或大端格式。
注释以"/*"开头,以"*/"结束。
可选组件通过将其包含在"[[ ]]" 双括号中来表示。
包含未解释数据的单字节实体属于opaque类型。
一个向量(一维数组)是一个同类数据元素的流。向量的大小可能在编写文档时指定或留待运行时确定。在任何情况下,向量的长度都是指字节数而非元素数。定义一个新类型T'(是一个固定长度的类型T的向量)的语法是:
T T'[n];
这里,T'在数据流中占据了n个字节,而n是多个类型T的大小。向量的长度并不包含在编码流中。
在下面的例子中,Datum被定义为协议不能理解的3个连续字节, 而Data是三个连续的Datum,共占据9个字节。
opaque Datum[3]; /* 三个未知字节 */
Datum Data[9]; /* 3个连续的3字节向量 */
变长向量的定义是通过指定一个合法长度的子范围来实现(使用符号<floor..ceiling>)。当这些被编码时,在字节流中实际长度是在向量的内容之前。这个长度会以一个数字的形式出现,并消耗足够多的字节以便表示向量的最大长度(ceiling)。一个实际长度是0的变长向量会被当做一个空向量。
T T'<floor..ceiling>;
在下面的例子中会强制要求一个向量必须包含300-400个字节的opaque类型数据,它不能为空。实际长度域占用两个字节,一个uint16,这足以代表数值400(见4.4节)。另外一方面,更长的向量可以描述多达800字节的数据,或400个uint16类型的元素,这个向量可以为空。它的编码会包含一个两字节的实际长度域设置在向量前。一个编码向量的长度必须是单个元素长度的偶数倍(例如,一个17字节长的uint16类型的向量是非法的)。
opaque mandatory<300..400>; /*长度域是2字节,不能为空 */
uint16 longer<0..800>; /* 0-400 16-bit 无符号整数 */
基本数字数据类型是一个无符号字节(uint8)。所有更大的数字数据类型都被组织成固定长度的字节序列并如4.1节中所描述的那样被串联,且同样是无符号的。下面是预定义的数字类型。
uint8 uint16[2];
uint8 uint24[3];
uint8 uint32[4];
uint8 uint64[8];
本篇规范中的所有的数值都是以网络字节序(大端)存储;一个uint32类型的十六进制字节01 02 03 04等于十进制数16909060。
需要注意的是在一些情况下(如DH参数)有必要将整数以不透明(译注:opaque)向量的形式表示。在这种情况下,它们代表无符号整数(即,最开始的0字节是不需要的,即使设置了最有意义的bit(译注:不明白这个bit是什么))。
另外一种少见的数据类型是枚举。一个枚举类型的域仅能表示定义时声明的值。每个定义都是一个不同的类型。只有相同类型的枚举能被指定或比较。一个枚举的每个元素必须被指定一个值,就像下面的例子所表明的。既然枚举类型的元素并不是有序的,它们能够被以任意顺序指定任意独一的值。
enum { e1(v1), e2(v2), ... , en(vn) [[, (n)]] } Te;
一个枚举在字节流中占据的空间足够存储其定义的最大有序数值。下面的定义会使用1个字节来表示Color类型的域。
enum { red(3), blue(5), white(7) } Color;
一个选择是指定一个值但不关联标记以强制定义枚举的大小,这样无需定义一个多余的元素.在下面这个例子中,Taste在字节流中会消耗2个字节, 但只能表示数值1,2,或4。
enum { sweet(1), sour(2), bitter(4), (32000) } Taste;
一个枚举类型的元素的名称被局限于定义的类型。在第一个例子中,对枚举的第二个元素的完全合格的引用是Color.blue,如果赋值的目标是被 很好地指定(译注:这句话的意思应该不会引起冲突,或叫二义性),则这样的格式是不需要的。
Color color = Color.blue; /* 过度指定, 合法 */
Color color = blue; /* 正确, 类型隐藏 */
对于不能转化为外部表示的枚举(原文:external representation),其数字信息会被忽略。
enum { low, medium, high } Amount;
出于方便,结构体类型可以由原始类型构建。每个规范声明了一个新的、独特的类型。定义的语法很像C语言:
struct {
T1 f1;
T2 f2;
...
Tn fn;
} [[T]];
结构体内的域可以用类型的名字来描述,使用类似于枚举的语法。例如,T.f2引用了前面定义的结构的第二个域。结构体的定义可以嵌套。
基于从环境中获得的知识,定义的结构体可以有一些变量。选择符必须是一个定义了结构体中变量取值范围的枚举类型。在select中声明的每个枚举元素必须有一个case条件。Case条件有受限的通过条件:如果两个case条件中间紧紧相连,它们中间没有域,则它们拥有相同的域。因此,在下面的例子中,"orange"和"banana"都包含V2,注意这是TLS 1.2中的一个新的语法。
变量结构体的体可以添加一个标签用于引用。通过这个机制变量可以在运行时被选择,并不会被描述语言所限制。
struct {
T1 f1;
T2 f2;
....
Tn fn;
select (E) {
case e1: Te1;
case e2: Te2;
case e3: case e4: Te3;
....
case en: Ten;
} [[fv]];
} [[Tv]];
例如:
enum { apple, orange, banana } VariantTag;
struct {
uint16 number;
opaque string<0..10>; /*变长*/
} V1;
struct {
uint32 number;
opaque string[10]; /* 固定长度 */
} V2;
struct {
select (VariantTag) { /*selector的值是隐晦的 */
case apple:
V1; /* 变量体, tag = apple */
case orange:
case banana:
V2; /*变量体, tag = orange or banana */
} variant_body; /* 变量的可选标签*/
} VariantRecord;
5个密码操作(数字签名,流密码加密,块密码加密,AEAD(authenticated encryption with)加密,公钥加密)分别被命名为:数字签名、流加密、块加密、aead加密和公钥加密。一个域的密码处理流程是通过将一个经过适当合适的关键字放在域类型规范之前来指定的。密码密钥由当前连接的状态所暗示的(见6.1节)
一个数字签名元素被编码为一个DigitallySigned结构:
struct {
SignatureAndHashAlgorithm algorithm;
opaque signature<0..2^16-1>;
} DigitallySigned;
algorithm域指定了所使用的算法(见7.4.1.4.1节对这个域的定义)。需要注意的是algorithm域的介绍是改变自以前的版本。signature是一个使用这些算法对内容元素进行的数字签名。这些内容本身不会出现在网络通路上但可以被简单计算出来。signature的长度由签名算法和密钥确定。
对于RSA签名,opaque向量包含使用RSASSA-PKCS1-v1_5签名方案(定义在[PKCS1]中)所生成的signature。 正如在[PKCS1]中所讨论的,DigestInfo必须是DER编码的[X680][X690]。对于没有参数的hash算法(包括SHA- 1),DigestInfo.AlgorithmIdentifier.parameters 域必须是NULL,但实现上必须接受有无参数和NULL参数。需要注意的是TLS早期版本使用了一个不同的RSA签名方案,这个方案不包含 DigestInfo编码。对于DSA,20字节的SHA-1hash会通过数字签名算法来直接运行,不需要添加额外的hash算法。这样产生了两个值,r和s。DSA签名 是一个opaque向量,像上面的一样,其内容是DER编码的:
Dss-Sig-Value ::= SEQUENCE {
r INTEGER,
s INTEGER
}
注意:在当前的术语中,DSA是指数字签名算法,DSS是指NIST标准。在原始的SSL和TLS规范中,DSS使用得很广泛。本文使用 "DSA"是指算法,"DSS"是中标准,在代码中使用"DSS"是考虑到历史的连续性。
在流密码加密中,明文是同源自一个密码学安全密钥的伪随机数生成器的相同数量的输出进行的异或运算。
在块密码加密中,每个明文块被加密为一个密文块。所有的密文块都以CBC(密码分组链接模式)模式生成。所有的分块加密的元素都会是密文块长度的精确倍数。
在AEAD加密中,明文同时被加密和进行完整性保护。输入可以是任意长度,aead加密输出通常比输入大,以容纳完整性检验值。
在公钥加密中,一个公钥算法所加密的数据只有使用匹配的私钥才能解密。一个公钥加密元素被编码成一个opaque向量<0..2^16-1>,这里的长度由加密算法和密钥指定。
RSA加密可以使用RSAES-PKCS1-v1_5加密机制实现,这个机制在[PKCS1]中定义。
在下面的例子中,
stream-ciphered struct {
uint8 field1;
uint8 field2;
digitally-signed opaque {
uint8 field3<0..255>;
uint8 field4;
};
} UserType;
内部结构(field3和field4)的内容被用作签名/hash算法的输入,然后整个结构体被流密码加密。这个结构体的长度按字节计算,等于field1和field2所用的2个字节,加上签名和hash算法用的2个字节,加上签名长度用的两个字节,加上签名算法输出的长度。签名的长度是已知的,因为签名所用的算法和密钥在编码或解码这个结构体之前就已经知道了。
出于规范性,固定类型的常量的定义可以通过声明一个所需类型的符号并赋值来实现。
不确定的类型(译注:原文是“Under-specified types”)(opaque,变长向量,和包含opaque类型的结构体)不能被赋值。多元素结构体或向量的赋值可以忽略域的名称。
例如:
struct {
uint8 f1;
uint8 f2;
} Example1;
Example1 ex1 = {1, 4}; /* 设置 f1 = 1, f2 = 4 */
TLS记录层使用一个有密钥的信息验证码(MAC)来保护信息的完整性。本文中定义的密码算法族使用了一个被称为HMAC(在[HMAC]中描 述)的MAC算法,它基于一个hash函数。如果必要的话其它密码算法族可以定义它们自己的MAC算法。
此外,为了进行密钥生成或验证,需要一个MAC算法对数据块进行扩展以增加机密性。这个伪随机函数(PRF)将机密信息(secret),种子和 身份标签作为输入,并产生任意长度的输出。
在本节中,我们基于HMAC定义了一个PRF。这个使用SHA-256 hash函数的PRF被用于所有的密码算法族,这些密码算法在本文中定义,或在本文之前、在TLS1.2的协商阶段就发表的TLS文献中定义。新的密码算 法族必须显式指定一个PRF,通常应该将SHA-256或更强的标准hash算法与TLS PRF一同使用。
首先,我们定义一个数据扩展函数,P_hash(secret, data),它使用一个hash函数扩展一个secret和种子,形成任意大小的输出:
P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
HMAC_hash(secret, A(2) + seed) +
HMAC_hash(secret, A(3) + seed) + ...
这里"+"是指级联。
A()被定义为:
A(0) = seed
A(i) = HMAC_hash(secret, A(i-1))
必要时P_hash可以被多次迭代,以产生所需数量的数据。例如,如果P_SHA256被用于产生80字节的数据,它应该被迭代3次(通过 A(3)),产生96字节的输出数据;最终迭代产生的最后16字节会被丢弃,留下80字节作为输出数据。
TLS的PRF可以通过将P_hash运用与secret来实现:
PRF(secret, label, seed) = P_<hash>(secret, label + seed)
label是一个ASCII字符串。它应该以严格地按照它被给出的内容进行处理,不包含一个长度字节或结尾添加的空字符。例 如,label"slithy toves"应该通过hash下列字节的方式被处理:
73 6C 69 74 68 79 20 74 6F 76 65 73
(译注:上述数据是字符串"slithy toves"的十六进制格式)
TLS记录协议是一个层次化的协议。在每一层中,消息都可能包含长度、描述、内容等域。记录协议承载被发送的消息,将数据分片为可管理的块,有选择地压缩数据,应用MAC,加密,传输最终数据。接收到的数据被解密,验证,解压缩,重组,然后传递给高层客户。
本文中描述了4个使用记录协议的协议:握手协议,告警协议,更改密码规格协议,和应用数据协议。为了支持TLS协议扩展,额外的记录内容类型也可以被记录协议支持。新的记录内容类型值被IANA在TLS内容类型注册(在12节描述)中分配。
应用不能发送本文没有定义的记录类型,除非经过一些扩展的协商。如果一个TLS实现接收到一个非期望的记录类型,它应该发送一个“unexpected_message”告警消息。
任何被设计用来使用TLS的协议必须严格地设计以处理可能的攻击。从实践上来说,者意味着协议设计者必须意识到TLS能够实现何种安全特性,无法提供或不能安全地依赖后者。
特别需要注意的是一个记录消息的类型和长度不能使用加密保护。如果这个信息本身是敏感的,应用设计者可能会希望采取一些措施(填充,覆盖流量(译注:原文是“cover traffic”))以减小信息泄露。
一个TLS连接的状态就是TLS记录协议的操作环境。