AES

AES - Advanced Encryption Standard

AES 是取代 DES 称为新标准的一种对称密码算法

AES 的选拔

AES 由 NIST (National Institute of Standards and Technology, 国家标准技术研究所) 组织公开竞选,最终所选拔的密码算法,成为美国的国家标准,即联邦信息处理标准(FIPS), 但最终成为一个世界性的标准。

AES 选拔对全世界公开,参加者提供密码算法的详细规格书、算法强度的证明和实现代码,由全世界的企业和密码学家共同完成评审。2000 年, Rijndael 算法被 NIST 从最终候选算法名单中选定成为 AES 标准。

被选为 AES 密码算法必须无条件地免费供全世界使用。

Rijndael 密码算法

两位比利时密码学家设计,也属于*分组密码算法 *(block cipher)

分组长度: 128 比特位

密钥长度: 128、192 和 256 比特

Rijndael 的加密和解密

和 DES 一样,Rijndael 算法由多轮构成,其中每一轮分为 SubBytes、ShiftRows、MixColumns 和 AddRoundKey 个步骤。并且使用了一种称为 SPN 结构。

1. SubBytes-逐字节替换

Rijndael 的分组是 128 比特位,即 16 字节。对 16 个字节进行 SubBytes 处理,就是将每个字节所代表的无符号整数值(0-255)作为索引,从一个拥有 256 个值的替换表 (S-Box, S盒) 中查找对应的值。简单来说,就是每个字节查表映射为新的值。

SubBytes

2. ShiftRows-平移行

将以 4 字节为单位的行 (row) 按照一定的规则向左平移,且每一行平移的字节数是不同的。图示为对其中一行进行处理的情形:

ShiftRows

3. MixColumns-混合列

对一个 4 字节的值进行比特运算,将其变为另一个 4 字节的值,图示为对其中一列(column)进行处理的情形:

MixColumns

4. AddRoundKey-轮密钥异或运算

将 MixColumns 的输出与轮密钥进行 XOR,图示为对其中一个字节进行处理的情形:

AddRoundKey

输入的所有比特在一轮中都会被加密,相比于 DES 的 Feistel 网络相比(每一轮只加密一半输入的比特位),加密所需的轮数更少,一般重复进行10 到 14 轮,而且其中的步骤,可以分别以字节、行和列为单位进行并行计算。

注:并行计算也是衡量密码算法的一个重要指标,并行意味着加密解密的速度块

加密的过程:

1
SubBytes  --> SbiftRows --> MixColumns --> AddRoundKey

解密,就是按照相反的顺序来进行就可以:

1
2
3
AddRoundKey --> InvMixColumns --> InvShiftRows --> InvSubBytes

* Inv 表示逆运算

Rijndael 的算法背后有着严谨的数学结构,明文到密文的过程可以全部用数学公式来表达,此外,轮密钥的计算也涉及到稍微复杂的过程,没有在这里涉及。

使用 openssl 和 JCA 进行加密解密的验证

opesnssl 加密和解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用 openssl 加密 'hello' 字符串
$ openssl enc -aes-128-cbc -e -base64 -salt -p <<< hello

salt=3631C9DA382CE487
key=421990ECB00E9A9B8372D0E46727F6AA
iv =5973EAA7D1C1D5240B78CB8A4458D663
U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=

# 使用 openssl 进行解密
$ openssl enc -aes-128-cbc -d -base64 -salt -p <<< U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=

salt=3631C9DA382CE487
key=421990ECB00E9A9B8372D0E46727F6AA
iv =5973EAA7D1C1D5240B78CB8A4458D663
hello

使用 JCA (Java Cryptography Architecture) 进行验证

openssl 会生成 8 个比特的 salt,并以 Salted__${salt} 附加在加密的密文前,一共占 16 个字节,验证如下:

1
2
3
4
$ echo -n "U2FsdGVkX182McnaOCzkh7BPMUJaKwGCIc8cAGAtbNw=" | base64 -d | hexdump -C
00000000 53 61 6c 74 65 64 5f 5f 36 31 c9 da 38 2c e4 87 |Salted__61..8,..|
00000010 b0 4f 31 42 5a 2b 01 82 21 cf 1c 00 60 2d 6c dc |.O1BZ+..!...`-l.|
00000020

所以实际加密后密文的 16 进制为:b04f31425a2b018221cf1c00602d6cdc

使用 JCA 进行解密验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class AESCryptoTest {

public static final String AES = "AES";
public static final String AES_CBC_PKCS_5_PADDING = "AES/CBC/PKCS5Padding";

public static void main(String[] args) {
String keyHex = "421990ECB00E9A9B8372D0E46727F6AA";
String ivHex = "5973EAA7D1C1D5240B78CB8A4458D663";
String cipherTextHex = "b04f31425a2b018221cf1c00602d6cdc";

byte[] key = HexUtils.fromHexString(keyHex);
byte[] iv = HexUtils.fromHexString(ivHex);
byte[] cipherText = HexUtils.fromHexString(cipherTextHex);

byte[] clearText = aesDecrypt(key, iv, cipherText);
System.out.println(new String(clearText, StandardCharsets.UTF_8));
}

@SneakyThrows
public static byte[] aesDecrypt(byte[] key, byte[] iv, byte[] cipherText) {
// constructor a secret key
SecretKeySpec secretKey = new SecretKeySpec(key, AES);

// init algorithm parameters with iv
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance(AES);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
algorithmParameters.init(ivParameterSpec);

// init a AES cipher with key and iv
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS_5_PADDING);
cipher.init(Cipher.DECRYPT_MODE, secretKey, algorithmParameters);

// decrypt
return cipher.doFinal(cipherText);
}
}

//最终输出 hello

JCA

JSSE Java Secure Socket Extension

JGSS Java Generic Security Services - kerberos

SASL Simple Authentication and Security Layer

JCE 使用和 JCA 一样的架构,JCE 应该被看做是 JCA 的一部分.

JDK 中的 JCA 包含两个组件:

  • 定义密码服务以及提供支持的框架
  • 实际提供实现的 provider, 如 Sun, SunRsaSign, SunJCE

JCA 设计原则

  • 实现独立性与互操作性
  • 算法独立性与可扩展

使用密码服务,例如数字签名,消息摘要等,并不需要担心实现的细节,甚至是组成这些基础概念的的算法。完全的算法独立性是做不到的,这里的意思就是不可能完全抽象出一个接口,屏蔽使用的细节,做到算法的独立性。JCA 提供了标准化的,特定类型相关算法的 API. 并且当有多个实现时,允许开发者指定特定的实现。

算法的独立性

通过密码引擎(cryptographic engines), 以及提供这些密码引擎功能的定义类实现独立性。这些类被称为 引擎类,如 MessageDigest, Signature, KeyFactory, KeyPairGenerator, Cipher 等类

实现的独立性

使用基于 procider 的架构实现的, provider(Cryptograpic Service Provider)在这里指的是实现一个或多个密码服务(如数字签名算法,消息摘要算法等)的包或一组包。应用程序可能只是简单的从安装的众多 provider 中请求实现了某个特定服务的特定类型对象,如实现了 DSA 签名算法的 Signature 对象,当然也可以自己指定从哪个特定的 provider 中获取。provider 对应用程序的使用是透明的。

可互操作性

的事项也可以正常工作,一个 provider 提供的某个服务实现产生的密钥,可以用于另一个 provider 相同服务提供的实现。再比如证书以及证书的验证等。其实就是算法都是相同的,JCA 对输入,输出进行了标准化,所以不同的 provider 实现都要遵循相同的标准,又是实现相同的算法,当然可以互相操作了

算法的可扩展性

只要是现有引擎类支持,该引擎类类的新算法可以很容易的被添加。比如,现在有一种新的消息摘要算法,实现之后就可以提供 provider ,应用中使用相同的引擎类 MessageDigest 类获取该新算法的实例,就可以使用了。

Cryptographic Service Providers

java.security.Provider 类是所有 provider 的基类,每一个 CSP 包含了这个类的一个实例,其中包含了 provider 的名称以及所有它实现的安全服务/算法的列表。 当需要某个特定算法的实例时,JCA 框架会查询所有的 provider,如果有匹配,就会创建它的实例。

providers 的包提供了一些广为人知的密码算法的具体实现。每一个 JDK 都默认安装并配置了一个或多个 provider。额外的 provider 可以被静态或动态的安装。客户端可以配置它们的运行时环境来指定 provider 的优先级顺序。就是 provider 被搜索的顺序。

使用 JCA,可以直接请求某个特定类型的对象和特定的算法,如 MessageDigest, 使用 SHA-256 摘要算法,这会从安装的 provider 中获得一个该算法的实现:

1
md = MessageDigest.getInstance("SHA-256");

如果需要,当然也可以指定某个具体的 provider

1
md = MessageDigest.getInstance("SHA-256","providerName");

JDK 中的密码服务实现分布在几个不同的 provider 中(Sun, SunJSSE, SunJCE, SunRsaSing), 其它 Java 运行时环境可能不包含这些 provider ,所以只有在确定某个 provider 安装的情况下,才可以指定使用哪个 provider 的实现。JCA 提供了 API 可以用户查询安装的 provider 以及它们提供的安全服务/算法。JCA 使得第三方的 provider 的添加也是很容易的, 如 BouncyCastle。

Provider 具体是怎么实现的

有如下的代码,目的是获取一个 AES 密码算法的实例:

1
2
Cipher c = Cipher.getInstance("AES");
c.init(ENCRYPT_MODE, key);

具体的流程大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
调用引擎类 Cipher 的静态工厂方法 getInstance

|
\|/

JCA 框架查询每一个安装的 provider, 并获取 Provider 类的一个实例(Provider视作是提供的算法的一个数据库)

|
\|/

最终找到一个合适的 CSP(provider),并指向了继承了 CipherSPI 的实现类 AESCipher,创建 AESCipher 的实例

|
\|/

AESCIpher 实例被封装在一个新创建的 Cipher 实例, Cipher 实例返回

当调用 init 方法时,Cipher 实例会将请求委托给 AESCiphrt对象对应的 engineInit 方法

KeyStore

keystore 是一种用来管理密钥和证书的数据库。应用在需要数据认证,加密以及签名时,就可以使用 keystore,在程序中,可以通过 KeyStore 类(位于 java.security 包下)。

keystore 类型/格式

  • pkcs12 - 基于 RSA PKCS12 (Personal Information Exchange Syntax Standard)标准, JDK9 默认以及推荐的格式
  • jks - 私有格式
  • jceks - 私有格式
  • pkcs11 - 基于 RSA PKCS11 标准,支持对密码令牌的访问,如硬件安全模块和智能卡

应用可以从不同的 provider 中选择不同类型的 keystore 实现。 KeyStore 类提供了接口来访问和修改一个 keystore 中的信息。这个类代表了一个内存中密钥和证书的集合,主要用来管理两种类型的条目:

  • 密钥 - Key

    密钥类型的条目保存了非常敏感的密钥信息,必须进行保护以避免未经授权的访问。通常,是一个私密的密钥 (secret key) 或者是一个私钥(private key)以及对其公钥进行认证的证书链。私钥以及证书链被一个指定的实体使用数字签名来进行自认证。例如:软件供应商对 JAR 文件进行数字签名,作为软件发布的一部分

  • 受信任的证书 - Trusted Certificate Entry

    这种类型的条目仅包含了一个个公钥证书(从属于其它实体), 被称为受信任的证书,因为 keystore 的拥有者新人证书中的公钥确实是属于证书 subject(owner) 所标识的实体

keystore 中的每一个条目都通过一个别名 (alias) 字符串表示. 对于私钥及其关联的证书链,这些字符串区分实体进行身份验证的不同方式。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    /**
* Use openssl tool to create pkcs12 file:
* $ openssl pkcs12 -export -in con_key_crt.pem -out sub_ca.p12 -CAfile root_ca.crt -name myKeyEntry
*
* con_key_crt.pem is a simple concatenation of sub_ca_key.pem and sub_ca.crt:
* $ cat sub_ca_key.pem sub_ca.crt >> con_key_crt.pem
*
*
* @throws Exception
*/
public static void keyStoreTest() throws Exception {
try (InputStream in = new FileInputStream("D:\\idea-workspace\\sub_ca.p12")) {
KeyStore keyStore = KeyStore.getInstance("pkcs12");
// 此处的密码用于验证 keystore 的完整性
keyStore.load(in, "123456".toCharArray());

// 打印所有的 alias 别名, 此例中只有一个 myKeyEntry (openssl -name 选项)
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
System.out.println("alias:" + aliases.nextElement());
}

// 获取 RSA 私钥
Key key = keyStore.getKey("myKeyEntry", "123456".toCharArray());
System.out.println(key.getAlgorithm());

KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(key, RSAPrivateKeySpec.class);
System.out.println(privateKeySpec.getModulus());

// 获取证书
Certificate certificate = keyStore.getCertificate("myKeyEntry");
System.out.println(certificate.toString());
}
}

// PKCS12 文件的生成详细内容见 PKCS 一章中的介绍,这里解析的 keystore 就是使用 openssl 创建的 PKCS12 文件, 包含了私钥及证书

Engine Classes and Algorithoms

引擎类提供了到某一个具体类型的安全服务的接口, 与 provider 及算法是独立的,提供了以下功能:

  • 密码服务操作(加密解密、数字签名,消息摘要等)

  • 密码材料(cryotographic material)的生成或转换(密钥和算法初始化参数)

  • 封装密码数据的对象(keystore 或证书),可以在抽象层的上层使用

可用的引擎类:

  • SecureRandom 用来生成随机数或伪随机数
  • MessageDigest 用来计算数据的消息摘要(hash, 单向散列函数)
  • Signature 通过密钥进行初始化,用来对数据进行签名以及验证数字签名
  • Cipher 通过密钥初始化,用来对数据进行加密,解密,有不同类型的密码算法:对称、非对称、PBE 等
  • Mac Message Authentication Codes 同样也是用来生成 hash 值的,但是要用到密钥,用来保护数据的完整性
  • KeyFactory 用来将非透明 Key 类型的密钥转换为特定密钥标准形式,反之亦然
  • SecretKeyFactory 用来将非透明 SecretKey 类型的密钥转换为特定密钥标准形式,SecretKeyFactory 是 KeyFactory 的特殊形式,专门用来生成秘密的堆成密码的密钥
  • KeyPairGenerator 用来生成一个新的公钥,私钥对,适用于特定的算法
  • KeyGeneraor 用来生成特定算法的密钥
  • KeyAgreement 使用在通信的两端,用来协商创建一个特定的密钥,使用在特定的密码操作中
  • AlgorithmParameters 用来存储的特定算法的参数,包含参数编码和解码
  • AlgorithmParameterGeneraor 用来生成适用于特定算法的一组 AlgorithmParameters
  • KeyStore 用来创建以及管理一个 keystore. 一个 keystore 是一个存储密钥的数据库,keystore 中的私钥还关联了一个与其相关的证书链,用来认证对应的公钥。keystore 可能包含来自信任实体的证书
  • CertificateFactory 用来创建公钥证书和 CRLs (Certificate Revtocation Lists) 证书撤回列表
  • CertPathBuilder 用来创建一个证书链(也称之为证书路径)
  • CertPathVlidator 用来对证书链进行验证
  • CertStore 用来从一个 repository 中检索证书和 CRLs

参考文献

[1] Java Cryptography Architecture (JCA) Reference Guide

[2] Java Security Standard Algorithm Names

对称密码

一次性密码本

一次性密码本也称为维纳密码(Vernam Cipher),特点:无条件安全,理论上是 ”无法破译” 的。

加密过程

将明文与一串随机的比特序列进行 XOR 运算,随机不是随便臆想出一个随机的序列,抛硬币的过程可以看作随机的过程,比如出现正面,可以看作比特位 1, 反面看作比特位 0. 密码学中有专门的随机数生成硬件和伪随机数生成器软件。

例如, 明文 midnight 的 ASCII 比特序列为:

1
2
3
echo -n "midnight" | hexdump -e '/1 "%02X "' | xargs -n 1 | while read dec; do echo  "ibase=16;obase=2;$dec" | bc | tr "\n" " " | sed 's/^/0/g'; done; echo

01101101 01101001 01100100 01101110 01101001 01100111 01101000 01110100 ====> midnight

产生一个和明文长度相同的 64 位比特序列,作为一次性密码本的密钥:

1
01001101 01001101 01010100 01111110 01101111 01100001 01000000 00010100 ====> 密钥

加密,即进行异或运算

1
2
3
4
5
	01101101 01101001 01100100 01101110 01101001 01100111 01101000 01110100 明文

01001101 01001101 01010100 01111110 01101111 01100001 01000000 00010100 密钥
---------------------------------------------------------------------------
00100000 00100100 00110000 00010000 00000110 00000110 00101000 01100000 密文

解密过程

解密就是加密的反向运算,即用密文和密钥进行 X0R 运算,就可以得到明文

1
2
3
4
5
6

00100000 00100100 00110000 00010000 00000110 00000110 00101000 01100000 密文

01001101 01001101 01010100 01111110 01101111 01100001 01000000 00010100 密钥
---------------------------------------------------------------------------
01101101 01101001 01100100 01101110 01101001 01100111 01101000 01110100 明文

一次性密码本是无法破译的

只要遍历密钥空间,总能找到进行加密解密的密钥(运算能力无穷大,可以在有限时间内),然后对密文界面,无法破译指的是没有办法判断解密后的内容是否是正确的的明文, 所有可能密钥的解密,可能会出现类似于 midnight, onenight 的单词,也可能会出现其他不规则的字符组合,作为一个破译者,没有办法定义哪一个是正确的明文。明文就对应着密钥,所以一次性密码本无法破译。

一次性密码本存在的问题

  • 密钥配送

    可以安全的发送密钥,那为什么不直接用来安全的发送明文?

  • 密钥保存

    密钥与明文一样长,保存明文转换为保存密钥的问题

  • 密钥重用

    不能重用用过的随机比特序列,否则,获取所有的机密通信将被全部解密

  • 密钥同步

    密钥长度与明文长度相同,明文过长,密钥的传输与通信不能有任何差错(比特位的丢失,错位等),否则,将不能解密

DES

DES (Data Encryption Standard) 是 1997 年美国联邦信息处理标准中采用的一种对称密码。DES 已经可以在短时间内被破译,所以除了兼容性,不应该再使用 DES 来直接加密

加密与解密

DES 属于分组密码(block cipher),以 64 比特位为一个单位来进行加密的,密钥长度为 64 位,实际只有 56 位,每隔 7 位会设置一个错误校验位,所以 DES 的密钥长度为 56 位. (分组密码的模式)

image-20210126210108366

DES 的结构

DES 的基本结构由 Horst Feistel 设计, 所以也称为 Feistel 网络, Feistel 结构, Feistel 密码, 并且应用到了其它的密码算法中. Feistel 中将每一次加密过程, 称为 (round), DES 是一种 16 轮循环的 Feistel 网络.

一轮的过程

加密的过程

image-20210126211458134

Feistel 网络中一轮的过程如上图所示, 输入被等分为左右两半,分别进行处理, 写作左侧和右侧, 加密后的左半部分写作”加密后的左侧”, 右侧称为右侧.

Feistel 网络中每一轮都需要使用一个不同的密钥, 所以这个密钥只在一轮中使用, 因此称为 子密钥 (subkey)

具体的步骤:

(1) 将输入的数据等分为左右两部分
(2) 将输入的右侧直接发送到输出的右侧
(3) 将输入的右侧发送到轮函数
(4) 轮函数根据右侧数据和子密钥,计算出 一串看上去是随机 的比特序列
(5) 将上一步得到的比特序列与左侧数据进行 XOR 运算, 将结果作为加密后的左侧

每一轮, 右侧部分是没有被加密的,因此需要使用不同的子密钥重复多轮, 每两轮之间将左侧和右侧的数据进行对调. 最后一轮结束后不需要对调。3 轮的Feistel 网络如下图所示:

image-20210126213116336

解密的过程

image-20210126213750078

用相同的子密钥运行两次 Feistel 网络就能够将数据还原, 利用的其实 XOR 异或的性质 (两个相同的数异或为 0), 多轮的情况是一样的:

image-20210126214210806

Feistel 网络的性质

  • 加密时无论使用任何函数作为轮函数都可以正确解密

    即便使用的轮函数的输出结果无法逆计算出输入的值(即该函数不存在反函数)也没有问题,可以被设计得任意复杂;Feistel 网络实际上就是从加密算法中抽取出“密码的本质部分”并将其封装成一个轮函数

  • 可解密性

    在 Feistel 网络的一轮中, 右半边部分没有进行任何处理,看起来时一种浪费,但却保证了可解密性,是解密过程中所必须的信息;

  • 加密和解密可以用相同的结构来实现

    由于加密解密可以用相同的结构来实现,因此用于实现 DES 算法的硬件设备的设计变得容易

三重 DES

三重 DES (triple - DES) 是为了增加 DES 的强度, 将 DES 重复 3 次的一种密码算法,也称为 TDEA (Triple Date Ecnryption Algorithm), 通常称为 3DES。

三重 DES 的加密过程:

image-20210126221848991

明文经过三次 DES 处理变为密文,所以三重 DES 的密钥长度是 56 * 3=168 比特. 三重 DES 并不是进行三次加密,而是 加密 --> 解密--> 加密 的过程, 此方法由 IBM 公司设计,目的是兼容普通的 DES, 当三重 DES 使用的所有密钥都相同时,三重 DES 就相等于普通的 DES , 因为 加密-->解密 就变为明文了。因此三重 DES 对 DES 具备向下兼容性

DES-EDE2: 密钥1和密钥3使用相同的密钥,密钥2使用不同的密钥

DES-EDE3: 密钥1,密钥2,密钥3全部使用不同的比特序列

三重 DES 的解密过程:

三重 DES 的解密过程和加密正好相反,以密钥3,密钥2,密钥1的顺序执行 解密-->加密-->解密 的操作

image-20210126221714656

关于 DES 与 3DES 的使用

由于 DES 的密文已经可以在短时间内被破译,处理解密以前的密文,不应该再使用 DES 了。三重 DES 由于处理速度不高,很少被用于新的用途。

在日本总务省和经济产业省 2013 年发布的《电子政府相关技术采购中参考的密码清单》中,“电子政府推荐使用的密码清单” 项中将 3-key Triple DES 作为 64 比特分组密码列了出来,但考虑到 IST SP 800-67 的规定,以及其事实性标准的地位,又在脚注中给出了“目前暂且允许使用”的描述

使用 openssl 通过 3DES 进行加密和解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 加密
$ cat seq.txt
1
2
3
4
5
6
7
8
9
10

$ openssl enc -e -des3 -a -salt -in seq.txt -out encrypted.txt
enter des-ede3-cbc encryption password:
Verifying - enter des-ede3-cbc encryption password:

$ cat encrypted.txt
U2FsdGVkX1+HbjKWGlQzlIM7EM/TFhzn8wzKBBWn91TBdUSH3bOCeQ==

# 解密
openssl enc -d -des3 -a -salt -in encrypted.txt -out decrypted.txt
enter des-ede3-cbc decryption password:

$ cat decrypted.txt
1
2
3
4
5
6
7
8
9
10

OAuth

OpenID Connect

User Managed Access (UMA)

资源拥有者向授权服务器进行身份认证,可以自由选择: 用户名/密码、加密证书、安全令牌、联合单点登录或其它方式

授权服务器可以保存授权决策,后续可以跳过授权环节,沿用前一次的授权决策

也可以通过客户端白名单或者黑名单的内部策略来否决用户的决策

令牌内省

记忆授权决策

授权码许可类型

隐式许可类型

客户端凭据许可类型

授权服务器和资源服务共享令牌数据库是一种简单可行的机制,但是如果授权服务器要对多个资源服务进行保护,这种共享就不合适,可以使用:结构化令牌和令牌内省

结构化令牌 - JWT

签名的 JWT

JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// JWT 头部,描述令牌剩余部分的信息
{
"typ": "JWT", // 表明令牌的第二部分(载荷)的类型
"alg": "none" // none 表示未签名的令牌
}

// 载荷, JWT 的载荷可以是任意的 JSON 对象
{
"sub": "1234567890", // 令牌主体, 令牌是关于谁的,一般设为资源拥有者的唯一标识
"name": "John Doe", // 特定于当前应用的新增字段,用户名称
"admin": true // 特定于当前应用的新增字段,是否为管理员
}

// 载荷, JWT 提供的一些可选的声明,可以为特定的应用新增字段
{
"iss": "", // 令牌颁发者,令牌的创建者,一般设为授权服务器的 URL
"sub": "", // 令牌主体, 令牌是关于谁的,一般设为资源拥有者的唯一标识
"aud": "", // 令牌的受众,令牌的接收者,包含受保护资源的 URI 或者能够接受该令牌的受保护资源,可以是字符串数组
"exp": 123456, // 令牌的过期时间戳,令牌什么时候过期,自 UNIX 纪元以来的秒数
"nbf": 123456, // 令牌的生效时间戳,令牌从什么时候生效,自 UNIX 纪元以来的秒数
"iat": 123456, // 令牌颁发的时间戳,令牌何时被创建,自 UNIX 纪元以来的秒数
"jti": "" // 令牌的唯一标识符。令牌颁发者创建的每一个令牌中唯一,密码随机值,这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击
}

通过 JOSE 库得到最终代签名的令牌

1
2
3
4
5
6
7
8
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImF1dGhzZXJ2ZXIifQ.eyJpc3MiOiJodH
RwOi8vbG9jYWxob3N0OjkwMDEvIiwic3ViIjoiOVhFMy1KSTM0LTAwMTMyQSIsImF1ZCI6Imh0d
HA6Ly9sb2NhbGhvc3Q6OTAwMi8iLCJpYXQiOjE0NjcyNTE5NjksImV4cCI6MTQ2NzI1MjI2OSwi
anRpIjoidURYMWNwVnYifQ.nK-tYidfd6IHW8iwJ1ZHcPPnbDdbjnveunKrpOihEb0JD5wfjXoY
jpToXKfaSFPdpgbhy4ocnRAfKfX6tQfJuFQpZpKmtFG8OVtWpiOYlH4Ecoh3soSkaQyIy4L6p8o
3gmgl9iyjLQj4B7Anfe6rwQlIQi79WTQwE9bd3tgqic5cPBFtPLqRJQluvjZerkSdUo7Kt8XdyG
yfTAiyrsWoD1H0WGJm6IodTmSUOH7L08k-mGhUHmSkOgwGddrxLwLcMWWQ6ohmXaVv_Vf-9yTC2
STHOKuuUm2w_cRE1sF7JryiO7aFRa8JGEoUff2moaEuLG88weOT_S2EQBhYB0vQ8A

OAuth2.0

客户端

  • client_id: 客户端标识字符串,由授权服务器分配,形式可采用开发者门户,动态客户端注册机等

  • client_secret: 保密客户端的共享密钥,用于与授权服务器交互时对自身进行身份认证,由授权服务器分配

其它配置:

  • 重定向 url
1
/callback
  • 授权端点

  • 令牌端点

  • 资源服务器, 受保护的资源端点

授权码许可方式获取访问令牌

  1. 将用户重定向至授权服务器的授权端点,并在请求的 URL 中包含适当的查询参数,如请求权限范围,重定向 URL (回调 URL, 即授权服务器返回授权码给客户端的端点),client_id 等。
1
2
3
4
5
6
7
8
# `code` 授权码许可类型
response_type: 'code',

# 客户端标识 ID
client_id: client.client_id,

# 客户端接收授权码的端点
redirect_uri: client.redirect_uris[0]
  1. 在授权服务器授权端点,用户对客户端进行授权(可能包含必要用户身份认证),授权结束,根据客户端传过来的重定向 URL 重定向至客户端端点, 并且携带了授权服务器生成的授权码
  1. 客户端拿到授权码,使用 POST 请求,并使用 Http Basic 认证(client_id作为用户名,client_secret 作为密码),将授权码以表单形式放入请求体中,发送至服务器令牌端点
1
2
3
4
5
6
// 请求头部,使用表单,使用 Http Basic 认证
var headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + encodeClientCredentials(client.client_id,
client.client_secret)
};
1
2
3
4
5
6
7
8
9
10

# 授权码许可类型
grant_type: 'authorization_code',

# 授权码
code: code,

# 此处不需要执行重定向
# 这里是 OAuth 规范约定必须指定和请求授权时相同的重定向 url
redirect_uri: client.redirect_uris[0]
  1. 请求成功时,授权服务器返回一个包含访问令牌等信息的 JSON 对象
1
2
3
4
{
"access_token": "987tghjkiu6trfghjuytrghj",
"token_type": "Bearer" // Bearer 令牌,持有该类型的令牌,就可以向受保护资源出示
}
  1. 客户端解析 JSON 对象,获取访问令牌值,并将其保存起来,以便以后使用

使用 state 参数添加跨站保护

为了避免恶意调用客户端重定向 URL 端点 (client.redirect_uris[0]),破解令牌,请求授权端点前,客户端生成一个随机字符串,并添加在请求授权端点的请求参数中 (state),授权端点在返回授权码时,会带着这个字符串,客户端进行比较,如果相同,表明是自己之前发送的授权请求,否则,应向客户提示异常。

使用令牌访问受保护资源

bearer 令牌,它意味着无论是谁,只要持有该令牌就可以向受保护资源出示

使用令牌向受保护资源发出调用请求,令牌的使用的方式有三种:

  1. HTTP Authorization 头部(OAuth 规范推荐)
1
'Authorization': 'Bearer ' + access_token
  1. 使用表单格式的请求体参数 (必须使得资源端点支持 POST 请求和表单)

  2. 使用 URL 编码的查询参数 (可能使得令牌暴漏在请求日志中)

刷新访问令牌

无需用户参与下获取新访问令牌的方法

步骤

1.授权服务器的令牌端点会将刷新令牌与访问令牌一起返回给客户端

1
2
3
4
5
{
"access_token": "987tghjkiu6trfghjuytrghj",
"token_type": "Bearer",
"refresh_token": "j2r3oj32r23rmasd98uhjrk2o3i"
}

2.客户端发送访问令牌失效时(使用令牌获取资源失败),使用刷新令牌重新请求访问令牌

1
2
3
# POST 请求, 同时头部也要添加 Basic 认证信息,并使用表单形式
grant_type: 'refresh_token',
refresh_token: refresh_token

如果刷新令牌有效,返回新的访问令牌和刷新令牌

1
2
3
4
5
{
"access_token": "IqTnLQKcSY62klAuNTVevPdyEnbY82PB",
"token_type": "Bearer",
"refresh_token": "j2r3oj32r23rmasd98uhjrk2o3i"
}

如果刷新令牌无效,提示错误, 回到最开始没有获取访问令牌的情况,提示用户重新进行获取与令牌的授权

受保护的资源端

使用 OAuth 保护 Web API

  1. 从传入的请求中解析出令牌
  2. 通过授权服务器验证令牌
  3. 根据令牌的权限范围做出响应,令牌的权限范围有多种

资源服务器如何对请求中传过来的令牌进行验证?

  1. 授权服务器与资源服务器共享令牌数据库
  2. 使用令牌内省(token introspection)的 Web 协议,由授权服务器提供接口,让资源服务器能在运行时检查令牌的状态
  3. 令牌内包含资源服务器能直接解析并理解的信息,如 JWT, 使用受加密保护的 JSON 对象携带声明信息

权限范围 scope

  1. 不同的权限范围对应不同的操作,如 GET\POST\DELETE 分别对应的 read write delete 权限

  2. 不同的权限范围对应不同的数据结果,例如,同一个 API 对权限范围不同的客户端返回不同的子集,如有一个销售统计 API, 每个大区只能请求自己大区的数据

  3. 不同的用户对应不同的数据结果

资源服务器可以根据令牌及其附属信息(如权限范围)直接做出授权决策。资源服务器还可以将访问令牌中的权限范围与其他访问控制信息结合起来,用于决定是否响应 API 调用以及响应什么内容。

资源服务器如何设计权限是比较灵活的,而且可以和其他控制信息结合,也是属于灵活的一种体现。客户端绝不会知道授权的用户与具体的权限范围,令牌本身就代表了资源拥有者的授权与权限范文,在资源服务器端,通过验证令牌以及获取令牌对应的访问权限,用户,以及其它控制信息,做出是否有要相应的决策以及相应什么内容。

授权服务器

授权服务器对用户进行身份认证、注册并管理客户端、颁发令牌。

授权

  1. 客户端将用户重定向至授权服务器授权端点 /authorize,并携带了必要的查询参数
1
2
3
4
5
6
7
https://api.weibo.com/oauth2/authorize?
client_id=3129654709& //客户端 id
redirect_uri=https://www.douyu.com/member/oauth/signin/weibo& //重定向客户端 uri
state=96b2a0b5a31566c5f63b2322f987736a& //state 防止恶意调用客户端重定向 uri
response_type=code& //授权码许可类型
approval_prompt=force& //授权提示?
scope=foo bar //客户端期望的权限范围
  1. 授权服务器验证客户端身份(client id),检查传入的 redirect_url 与注册时的是否一致;验证请求的权限范围是否符合要求(提前为每个客户端限定了权限范围),如果都满足要求,生成一个随机字符串 , 并将其与客户端请求的查询参数保存为一个映射;渲染一个授权表单,提供授权或拒绝授权,以及权限范围的选择, 并将随机字符串 ,嵌入到表单中(表示未完成授权,为授权页面提供了简单的防跨站请求伪造保护,防止对服务器端授权端点的恶意请求)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 提交的表单数据
POST /approve HTTP/1.1
Host: localhost:9001
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0)
Gecko/20100101 Firefox/39.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost:9001/authorize?response_type=code&scope=foo&client_id=
oauth-client-1&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&state=
GKckoHfwMHIjCpEwXchXvsGFlPOS266u
Connection: keep-alive

//reqid 即授权服务器生成的随机值,approve 表示用户的授权许可
reqid=tKVUYQSM&approve=Approve
  1. 用户同意授权,表单提交到授权码生成端点 /approve,对提交表单的请求验证 (随机值),权限范围进行验证(表单请求可能会被篡改,所以需要再次验证),生成授权码 code,存储在授权服务器上,并保存与之对应的权限范围(在令牌端点可以取出来,与访问令牌形成对应关系),客户端请求,并将其通过客户端请求授权时的重定向 uri 重定向到客户端,并且在查询参数上应用了客户端的 state 值(对客户端提供跨站保护,虽然不要求客户端传递该参数,但是要求授权服务器只要收到该参数就返回它)

令牌颁发

客户端身份认证,client_id, client_secret

授权许可类型 grant_type 检查, 如 authorization_code

如果是授权许可码类型,取出授权许可码 code, 并查询之前保存的 code 与客户端请求信息缓存,验证它确实是为该客户端生成的(对比 client_id)

客户端匹配成功,生成一个访问令牌,并将其保存

向客户端返回访问令牌,令牌类型

1
2
access_token: access_token,
token_type: 'Bearer'

支持刷新令牌,刷新令牌只在授权服务器上使用,访问令牌只在受保护的资源上使用

1
2
refresh_token: refresh_token,
client_id: clientId

授权许可类型

除了授权码许可类型外的许可类型

1 隐式许可类型

直接运行在浏览器内的客户端,此时,客户端没有必要再通过授权码获取访问令牌,因为授权码是通过浏览器(前端信道)发送给客户端的,对于浏览器来说是可见的(如果是 web 应用,是直接发送给客户端服务器的)。直接请求授权端点, 而不是令牌端点来获取访问令牌,其它流程与授权码许可类型相似,客户端 id 检查,授权端点检查权限范围,资源拥有者身份认证,并验证对请求的批准,生成令牌,在授权端点响应中,将令牌附在客户端重定向 URI 片段中 。

1
2
3
4
5
# 隐式许可类型下,客户端访问授权端点时的 response_type
response_type=token

# 授权端点响应重定向时的 URI, 附件了 access_token 和 token_type
GET /callback#access_token=987tghjkiu6trfghjuytrghj&token_type=Bearer

限使用隐式许可类型的限制

  • 无法持有客户端密钥,无法对浏览器隐藏密钥
  • 只使用授权端点,而不是用令牌端点 (不要求客户端在授权端点的身份认证),影响安全等级
  • 不可用于获取刷新令牌,浏览器内的应用运行的特点:短暂,没有必要

2 客户端凭据许可类型

后端系统通信的场景,并不代表某个特定用户,没有明确的资源拥有者。这种场景下,相当于没有用户对客户端授权。

客户端凭据许可类型,只使用后端信道,客户端代表自己从令牌端点获取令牌,此时客户端向授权服务器的令牌端点发出令牌请求。

1
2
3
4
5
6
7
# 客户端身份认证 head:客户端 ID 和密钥
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

# 客户端凭据许可类型的授权类型
grant_type: 'client_credientials'
# 也可以使用 scope 指定请求的权限范围,与授权码或隐式许可流程在 授权端点 上使用的 scope 参数一样
scope: foo

客户端直接向授权服务器进行身份认证,授权服务器向客户端颁发访问令牌,客户端能随时获取新令牌,无需单独的资源拥有者参与,因此也没有必要使用刷新令牌

特点

  • 客户端凭据类型没有任何直接的用户交互,是为可信的后端系统直接访问服务准备的

    因此对于交互式与非交互式客户端,最好进行区分,指定不同的权限范围

3 资源拥有者凭据类型

客户端通过后端信道使用用户名和密码换取访问令牌, 资源拥有者直接与客户端进行交互, 而不是授权服务器, 只使用令牌端点. 授权服务器验证客户端的身份, 从收到的请求中取出用户名和密码,并与本地存储的用户信息对比, 验证权限范围, 如果都匹配,则授权服务器向客户端颁发令牌, 刷新访问令牌。

1
2
3
4
5
6
7
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

# 客户端收集用户的用户名和密码,发送至授权服务器的令牌端点
grant_type=password
scope=foo bar
username=alice
&password=secret

虽然这种模式下客户端直接获取了用户的认证信息, 但是后续的请求受保护资源使用的是访问令牌, 比每次请求都直接使用用户凭据要好的多, 但这仍然是一种不不好的许可类型, 请不要在现实中使用.

4 断言许可类型

官方扩展许可类型,客户端得到一条结构化的且被加密保护的信息-断言, 使用断言向授权服务器换取令牌。断言必须由信任的认证机构提供. 这种许可类型只使用后端信道, 没有明确的资源拥有者参与. 断言一般来自第三方, 客户端可以不知道断言本身的含义. 客户端向授权服务器的令牌端点发送 HTTP POST 请求, 将断言作为参数传递给服务器. 客户端一样要进行身份认证

两种标准化的断言: 安全断言标记语言 (SAML); JSON Web Token (JWT)

1
2
3
4
5
6
Authorization: Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x

grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer

# 示例断言
assertion=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InJzYS0xIn0.eyJpc3MiOiJodHRwOi8vdHJ1c3QuZXhhbXBsZS5uZXQvIiwic3ViIjoib2F1dGgtY2xpZW50LTEiLCJzY29wZSI6ImZvbyBiYXIgYmF6IiwiYXVkIjoiaHR0cDovL2F1dGhzZXJ2ZXIuZXhhbXBsZS5uZXQvdG9rZW4iLCJpYXQiOjE0NjU1ODI5NTYsImV4cCI6MTQ2NTczMzI1NiwianRpIjoiWDQ1cDM1SWZPckRZTmxXOG9BQ29Xb1djMDQ3V2J3djIifQ.HGCeZh79Va-7meazxJEtm07ZyptdLDu_Ocfw82F1zAT2p6Np6Ia_vEZTKzGhI3HdqXsUG3uDILBv337VNweWYE7F9ThNgDVD90UYGzZN5VlLf9bzjnB2CDjUWXBhgepSyaSfKHQhfyjoLnb2uHg2BUb5YDNYk5oqaBT_tyN7k_PSopt1XZyYIAf6-5VTweEcUjdpwrUUXGZ0fla8s6RIFNosqt5e6j0CsZ7Eb_zYEhfWXPo0NbRXUIG3KN6DCA-ES6D1TW0Dm2UuJLb-LfzCWsA1W_sZZz6jxbclnP6c6Pf8upBQIC9EvXqCseoPAykyR48KeW8tcd5ki3_tPtI7vA

授权服务器进行断言解析,检查加密保护, 确定生成何种令牌. 断言可以表示很多不同的信息: 资源拥有者的身份,被允许的权限范围. 授权服务器决定接受哪些断言并为断言的含义制定解释规则. 最终生成访问令牌返回.

基于 OAuth 的身份认证协议:OpenID Connect