JOSE

Jose JSON 对象的加密与签名

创建证书并验证

使用 openssl 验证证书

证书可以具有层次结构,认证机构的公钥可以由其它认证机构施加数字签名,生成认证机构的公钥证书,一个认证机构的公钥来验证另一个认证机构的公钥,会最终形成一个认证链,认证链的终点称为根CA (Root CA). 根CA机构为自己的公钥进行数字签名,称为自签名(self-signature)

使用 openssl 生成两层结构的 CA

1
2
3
4
5
6

Root CA -----> Sub CA -------> Server Certificate

Root CA 自签名为自己颁发证书
Root CA 为 Sub CA 颁发证书(Root CA 私钥对 Sub CA 公钥进行数字签名)
Sub CA 为 Server 颁发证书 (Sub CA 私钥对 Server 的公钥进行数字签名)

具体命令

  1. 生成 Root CA 自签名
1
2
3
4
5
6
7
8
9
10
# req 表示 csr 证书签名请求
# -nodes 表示对密钥不加密
# -newkey 生成新的密钥,使用的公钥密码的类型和参数
# -keyout 密钥输出文件
# -x509 输出一个x509证书,而不是一个 csr 文件
# -days 证书有效期
$ openssl req -nodes -newkey rsa:2048 -keyout root_ca_key.pem -out root_ca.crt -x509 -days 365

root_ca_key.pem # Root CA 密钥文件
root_ca.crt # Root CA 证书文件
  1. 生成 Sub CA 的密钥,CSR 文件,以及通过 Root CA 为其颁发证书
1
2
3
4
5
6
7
8
9
10
11
# 生成 Sub CA 的密钥,CSR 文件
$ openssl req -nodes -newkey rsa:2048 -keyout sub_ca_key.pem -out sub_ca.csr

sub_ca.csr # sub CA CSR 证书签名请求文件
sub_ca_key.pem # sub CA 密钥文件

# 通过 Root CA 为其颁发证书
$ openssl x509 -req -in sub_ca.csr -days 365 -CA root_ca.crt -CAkey root_ca_key.pem -CAcreateserial -out sub_ca.crt -extfile ext.cnf

$ cat ext.cnf
basicConstraints=critical,CA:TRUE

上述的 -extfile 指定了 x509 扩展文件(extentisons) ,其中指明了颁发的证书作为 CA. 可以通过查看证书的内容看到:

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
$ openssl x509 -in sub_ca.crt -noout -text -purpose -ext basicConstraints

Certificate:
Data:
Version: 3 (0x2)
Serial Number:
33:7a:a8:42:c1:0a:39:59:55:fe:21:ab:88:a0:15:7d:d2:19:ac:32
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = CN, ST = Sichuan, L = Chengdu, O = CdTech, OU = IT, CN = www.CdTech.com, emailAddress = CdTech@CdTech.com
Validity
Not Before: Jan 31 11:53:54 2021 GMT
Not After : Jan 31 11:53:54 2022 GMT
Subject: C = CN, ST = Sichuan, L = Luzhou, O = LuzhouTech, OU = IT, CN = www.LuzhouTech.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:ad:a7:b4:4e:57:7e:a8:2b:95:49:79:06:bc:9e:
60:04:a4:2c:4f:45:dd:47:69:7c:bd:1f:88:14:3a:
b3:48:6a:80:bb:ed:97:99:eb:ab:b9:bf:c9:51:c3:
......
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
6b:b8:19:ea:77:7e:75:bd:22:3f:85:d0:17:84:b2:3e:cc:7a:
b1:80:b3:26:25:26:27:1b:42:b6:1d:90:db:40:41:1f:17:90:
e1:36:2e:b4:70:89:23:52:d8:af:86:98:2a:54:ba:09:8a:03:
......

最早进行测试的时候,没有指定该扩展,通过 sub CA 的私钥对 Server 进行签发证书后,使用 openssl verify 会验证失败,所以这里猜想,应该是没有指定 CA:TRUE 的扩展,所以验证的时候,不会将 sub CA 作为一个 CA 对待。可以通过 openssl x509v3_config 查看更详细的内容。serverfault 中的该问题 openssl invalid CA certificate 应该和这里是一样的问题

  1. 生成 Server 的密钥,CSR 文件,并通过 sub CA 私钥为其颁发证书
1
2
3
4
5
6
7
8
$ openssl req -nodes -newkey rsa:2048 -keyout server_key.pem -out server_ca.csr

server.csr
server_key.pem

$ openssl x509 -req -in server_ca.csr -days 365 -CA sub_caa.crt -CAkey sub_ca_key.pem -CAcreateserial -out server.crt

server.crt # server 的公钥证书,由 sub CA 进行数字签名

证书验证

1
2
3
# 根证书验证(自签名)
$ openssl verify -CAfile root_ca.crt root_ca.crt
root_ca.crt: OK
1
2
3
# sub CA 证书验证
$ openssl verify -CAfile root_ca.crt sub_ca.crt
sub_ca.crt: OK
1
2
3
4
5
# server 证书验证
cat sub_ca.crt root_ca.crt > chain.crt

openssl verify -CAfile chain.crt server.crt
server.crt: OK

安装证书

Linux: Installed CA certificate as trusted certificate

1
2
3
4
5
6
7
sudo mkdir /usr/share/ca-certificates/extra

sudo cp root_ca.crt /usr/share/ca-certificates/extra/root_ca.crt

sudo dpkg-reconfigure ca-certificates

sudo update-ca-certificates

*使用空格选中要安装的证书 root_ca.crt,回车,就可以安装,安装完后,再对 root_ca.crt sub_ca.crt 进行验证,就不要再指定 -CAfile了,因为已经将根证书安装在系统中称为信任的证书

image-20210131213446735

参考文献

[1] openSSL certificate-verification on Linux

[2] openssl invalid CA certificate

[3] openssl man x509

[4] openssl man x509v3 config

[5] verify-a-certificate-chain-using-openssl-verify

[6] Ubuntun 的证书配置

[7] 如何制作 CSR 文件

[8] 验证证书链

[9] openssl 证书操作

[10] 深入剖析 RSA 密钥原理及实践

PBE

微博-密码算法

基于口令的密码

基于口令的密码(Password Based Encryption, PBE) 是一种根据口令生成密钥,并使用该密钥进行加密的方法。加密和解密使用同一个密钥(即使用对称密码算法),PKCS #5 (RFC 2898) 规范描述了其实现细节,广泛使用的密码工具都对其提供了实现,如 Java 的 javax.crypto 包,密码软件 PGP, openssl 等。

PBE 的意义

要确保消息的机密性? —-> 使用密钥 (CEK) 进行加密

如何确保密钥的机密性?—-> 用另一个密钥 (KEY) 对密钥进行加密

如何确保另一个密钥 (KEY) 的机密性? —-> 继续使用第三个密钥?死循环 ?

使用口令和盐来生成密钥 (KEK) 吧,盐可以和加密后的密钥(CEK)一起保存在磁盘上,密钥(KEK)就可以丢掉了

口令就记在脑子里吧

如果不保存一般使用的密钥,靠人类记忆没有规律,冗长的密钥是十分困难的。

PBE 的加密与解密

pbe

* PBE 的加密过程

PBE 加密

  1. 生成 KEK, 使用伪随机数生成器生成 盐 (salt) ,盐就是一个随机数,用于防御字典攻击,将盐与用户输入的口令一起作为单向散列函数的输入,最终得到的散列值就是 KEK, 用来对密钥进行加密的密钥
  2. 利用伪随机数生成器再生成会话密钥 CEK, 用 KEK 对 CEK 进行加密,并和盐保存在安全的地方。KEK 就可以丢弃了,因为只需要盐和口令就可以重建 KEK
  3. 使用会话密钥对消息进行加密

PBE 解密

  1. 重建 KEK ,利用保存的盐和用户再次输入的口令作为散列函数的输入,得到 KEK
  2. 利用 KEK 解密之前加密的 CEK, 得到会话密钥 CEK
  3. 使用 CEK 对密文进行解密

盐的作用

盐是用来防御字典攻击的,字典攻击简单来说,就是攻击者提前准备好一组可能的口令,并计算好它们的摘要,当它们窃取加密后的会话密钥后,通过将准备好的候选摘要 KEK,尝试进行破解。主要是大量的用户使用了字典的词语来设置他们的密码,所以给了攻击者机会。当使用了盐后,KEK 的可能数量会随之增大,事先生成候选的 KEK 数量会变得很大,增加了破译的难度。

拉伸

生成 KEK 时,多次使用单向散列函数可以提高安全性,即将输出再次作为单向散列函数的输入,反复多次,一般建议最少 1000 次,目的也是增加攻击者破译的难度。这种多次迭代的方法称为拉伸 (stretching)

passwordEncryption

PKCS #5

RFC 8018 (Obsoletes 2898) Password-Based Cryptography Specification Version 2.1中详细描述了 PBE 的实现步骤。

PBKDF

PBKDF 即 Password Based Key Derivation Functions, 基于口令的密钥生成函数,仅包含密钥生成的定义,不包含加密和解密的过程。

PBKDF1

密钥生成过程中应用的是一个单向散列函数(hash function),如 MD2, MD5,SHA-1等, 与 PKCS #5 v1.5 中的定义兼容, 受限于单向散列函数输出的长度,生成密钥的长度也受到限制,只出于兼容性的需求使用。

PBKDF2

密钥过程中应用的是一个伪随机数生成函数(pseudorandom function), 如 HMAC with SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, and SHA-512/256,推荐在新的应用中使用

PBES

PBES 即 Password Based Encryption Schemes, 即 PBE 的策略,其实就是 PBKDF 与加密和解密过程相结合,完成密钥的生成,对消息的加密和解密。

PBES1 PBKDF1 与对称分组密码的结合

PBES2 PBKDF2 与对称分组密码的结合

How to encrypt user passwords

jasypt-How to encrypt user passwords

使用 openssl 与 JCA 进行实践

使用 openssl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
guo@DESKTOP-4L69AND:/mnt/e/learning-dir/shell-learning$ echo -n 'hello' | openssl enc -aes-128-cbc -e -base64 -pbkdf2 -iter 1000 -S 3631C9DA382CE487 -p

# 输入 password
enter aes-128-cbc encryption password:
Verifying - enter aes-128-cbc encryption password:

# Salt 盐
salt=3631C9DA382CE487

# pbkdf2 生成的密钥 secret key
key=1004087F17C38D06C8B24CA69175984C

# iv 初始化向量
iv =5FFD9210A708073E5AB718800F9958E6

# 加密后的密文 Salted__${salt}${cipherText}
U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=

同样使用 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package org.learn.something.security.crypto;

import lombok.SneakyThrows;
import org.apache.tomcat.util.buf.HexUtils;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.spec.KeySpec;
import java.util.Base64;

/**
* @author guo
* @date 2021/4/11
*/
public class PBECryptTest {

public static void main(String[] args) {
byte[] plainText = "hello".getBytes(StandardCharsets.UTF_8);

// create key
char[] password = "12345678".toCharArray();

// salt can be acquired by Random generator
byte[] salt = HexUtils.fromHexString("3631C9DA382CE487");
int iter = 1000;
int keyLength = 128;

// init algorithm
// iv can be acquired by Random generator
byte[] iv = HexUtils.fromHexString("5FFD9210A708073E5AB718800F9958E6");

// ==============================================================================
// 1. use pbkdf2 to generate 128 bits key
// 2. use key and AES to encrypt the message
byte[] key = derivePbkdf2Key(password, salt, iter, keyLength);
byte[] cipherText1 = encryptTest(plainText, key, iv);
opensslOutput(salt, cipherText1);


// ==============================================================================
// use PBE to encrypt the message so we don't have to care about the derived key
byte[] cipherText2 = pbeEncryptTest(plainText, password, salt, iter, iv);
opensslOutput(salt, cipherText2);
}

/**
* openssl output format: Salted__${salt}${cipherText}
*
* @param salt
* @param cipherText
*/
private static void opensslOutput(byte[] salt, byte[] cipherText) {
byte[] finalBytes = new byte["Salted__".length() + salt.length + cipherText.length];
System.arraycopy("Salted__".getBytes(), 0, finalBytes, 0, "Salted__".length());
System.arraycopy(salt, 0, finalBytes, "Salted__".length(), salt.length);
System.arraycopy(cipherText, 0, finalBytes, salt.length + "Salted__".length(), cipherText.length);
System.out.println("openssl output format: " + Base64.getEncoder().encodeToString(finalBytes));
}

/**
* pbkdf2
*
* @return
*/
@SneakyThrows
private static byte[] derivePbkdf2Key(char[] password, byte[] salt, int iter, int keyLength) {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

KeySpec pbeKeySpec = new PBEKeySpec(password, salt, iter, keyLength);
SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);

byte[] encodedKey = secretKey.getEncoded();
System.out.printf("algorithm: %s, key(hex format): %s\n", secretKey.getAlgorithm(), HexUtils.toHexString(encodedKey));
return encodedKey;
}

@SneakyThrows
public static byte[] encryptTest(byte[] plainText, byte[] key, byte[] iv) {
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");

AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
algorithmParameters.init(ivParameterSpec);

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey, algorithmParameters);

return cipher.doFinal(plainText);
}

@SneakyThrows
public static byte[] pbeEncryptTest(byte[] plainText, char[] password, byte[] salt, int iter, byte[] iv) {

KeySpec pbeKeySpec = new PBEKeySpec(password, salt, 1000, 128);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_128");
SecretKey secretKey = factory.generateSecret(pbeKeySpec);

Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_128/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, 1000, ivParameterSpec);

cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
return cipher.doFinal(plainText);
}
}
1
2
3
algorithm: PBKDF2WithHmacSHA256, key(hex format): 1004087f17c38d06c8b24ca69175984c
openssl output format: U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=
openssl output format: U2FsdGVkX182McnaOCzkhx9oK2fDQKq6pIwPZD9892Q=

参考阅读

[1] 图解密码技术

[2] PKCS #5 rfc8018

[3] jasypt-How to encrypt user passwords

[4] openssl enc command

[5] SecretKeyFactory Algorithms

[6] 廖雪峰-口令加密算法

[7] Deriving a secret from a master key using JCE/JCA

SpringBoot Https

Spring Boot 中使用 https

在 Spring Boot 中了指定证书路径以及相关的信息后, 启动应用后, 通过 https 访问, 可以在左上角 不安全 处点击查看服务器证书信息

证书基本信息

image-20210219213950292

证书详细信息

image-20210219214017725

证书路径 Certificate Path/证书链 Certificate chain

image-20210219214038926

Spring Boot 使用 SSL-HTTPS

ASN.1

ASN.1

简单介绍

ASN.1 (Abstract Syntax Notation dot one) ,用来描述通信协议中传输的数据,定义了抽象数据类型规范的形式, 具有健壮性, 准确性, 独立于编程语言,独立于硬件和操作系统, 可以通过简单类型组成复杂的数据类型。

例如定义联系人 Contact 类型的语法

1
2
3
4
Contact ::= SEQUENCE {
name VisibleString,
phone NumericString
}

上述的例子中定义了一个联系人的结构, 具有这样结构的联系人 name = ‘Josn Smith’, phone = 987 6543210可以通过 AS1.1 的编码规则 (后面会讲到) 序列化为二进制或文本格式, 便于进行传输

1
2
        JSON Encoding Rules (JER)  
{ "name" : "John Smith", "phone" : "987 6543210" }

完整定义的示例

购买订单类型的 ASN.1 定义

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
-- 所有的定义都需要包含在一个模块(module)中, module 是以 'BEGIN' 关键字开始, 'END' 关键字结束
-- ASN.1 中一个 Tag 是消息中每个组件的一个内部标识符(internal identification). Tag 都是唯一的, 可以直接指定,但最好还是使用 AUTOMATIC TAGS, 易于阅读和理解, 较少歧义性和冲突性的可能

MyShopPurchaseOrders DEFINITIONS AUTOMATIC TAGS ::= BEGIN

PurchaseOrder ::= SEQUENCE {
dateOfOrder DATE,
customer CustomerInfo,
items ListOfItems
}

CustomerInfo ::= SEQUENCE {
companyName VisibleString (SIZE (3..50)),
billingAddress Address,
contactPhone NumericString (SIZE (7..12))
}

Address::= SEQUENCE {
street VisibleString (SIZE (5 .. 50)) OPTIONAL,
city VisibleString (SIZE (2..30)),
state VisibleString (SIZE(2) ^ FROM ("A".."Z")),
zipCode NumericString (SIZE(5 | 9))
}

ListOfItems ::= SEQUENCE (SIZE (1..100)) OF Item

Item ::= SEQUENCE {
itemCode INTEGER (1..99999),
color VisibleString ("Black" | "Blue" | "Brown"),
power INTEGER (110 | 220),
deliveryTime INTEGER (8..12 | 14..19),
quantity INTEGER (1..1000),
unitPrice REAL (1.00 .. 9999.00),
isTaxable BOOLEAN
}
END

ASN.1 基础数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
-- BOOLEAN
doorOpen BOOLEAN ::= TRUE

-- INTEGER
speed INTEGER (0..60) ::= 40

--VisibleString
LineOfText ::= VisibleString

--IA5String
TextWithLayout ::= IA5String

--MORE ARE NOT LISTED HERE

Most Common ASN.1 Types

Constaints 约束

ASN.1 的 schema 定义中可以包含约束, 用来限制给定字段或类型的有效值集合, 通过使用约束, 也可以生成一个更紧凑的编码, 例如 INTEGER(0..200) 的值用一个字节进行编码就够了

基础约束

字母表约束

1
ardToReadChars ::= IA5String (FROM("8BI10OD5S"))

模式(类似于正则表达式)

1
LicensePlate ::= IA5String (PATTERN "[0-9]#4(-[A-Z]#2)?") -- NNNN[-NN]

值的大小,长度约束

1
2
LicensePlate ::= IA5String (SIZE (4..7))
CarPark ::= SEQUENCE SIZE (1..25) OF LicensePlate

值取值范围约束

1
CarSpeed ::= INTEGER (0..200)

单值约束

1
2
3
4
WarningColors ::= UTF8String ("Red" | "Yellow")
InfoColors ::= UTF8String ("Blue" | "White")
CitySpeedLimit ::= INTEGER (25 | 30 | 40)
HighwaySpeedLimit ::= INTEGER (40 | 50 | 60 | 70)

包含子类型

1
2
3
SignColors ::= UTF8String (InfoColors UNION WarningColors)
SpeedLimitSigns ::= INTEGER (CitySpeedLimit | HighwaySpeedLimit | 10 | 65)
RuralSpeedLimit ::= INTEGER (CitySpeedLimit INTERSECTION HighwaySpeedLimit)

编码约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PerInside ::= OCTET STRING (
CONTAINING Doc
ENCODED BY { joint-iso-itu-t asn1(1)
packed-encoding(3)
basic(0)
unaligned(1)})

pdf OBJECT IDENTIFIER ::= { iso(1)
member-body(2)
us(840)
adobe(113583)
acrobat(1)}

Doc ::= OCTET STRING (ENCODED BY pdf)

更复杂的字段条件依赖约束

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

Schema DEFINITIONS AUTOMATIC TAGS ::= BEGIN

Address ::= SEQUENCE {
street-address UTF8String,
country UTF8String -- see a note below,
postal-code UTF8String
} ((WITH COMPONENTS {
...,
country ("USA"),
postal-code (PATTERN "[0-9]#5(-[0-9]#4)?")
}
| WITH COMPONENTS {
...,
country ("Canada"),
postal-code (PATTERN "[0-9][A-Z][0-9]
[A-Z][0-9][A-Z]")
}
| WITH COMPONENTS {
...,
country ("Netherlands"),
postal-code (PATTERN "[0-9]#4 [A-Z]#2")
}
))

END

Encoding Rules - 编码规则

编码规则描述了应该将 ASN.1 中定义的值如何进行编码以进行传输(被转换为字节进行传输,或者反之,将字节转为 ASN.1 中的值) ,并且不受机器,编程语言,应用程序表示的影响。每一种编码规则都有其特点,例如在压缩方面的,解码速度方面的。任何一种编码规则都能表示要进行交换的信息,只是使用场景不同

BER, DER, CER

BER (Basic Encoding Rules) 是最古老的编码规则,使用 TLV (Tag-Length-Value) 的格式对所有的信息进行编码,即先发送一个标签(Tag)表明数据的类型,如 SEQUENCE,接着是数据的长度(Length), 然年后是实际的数据,DER (Distinguished Encoding Rules) 以及 CER (Canonical Encoding Rules) 是 BER 的子集,消除了 BER 提供的一些灵活性。DER 通常使用在于安全相关的应用中,例如 X.509 数字证书。

OER

OER (Octet Encoding Rules) - 最快的 ASN.1 编码规则,与 PER (Packed Encoding Rules) 一样,通过利用 ASN.1 的 schema 的信息表示的优势来限制包含类每个编码消息的信息数量,生成紧凑的编码输出。聚焦于编码与解码的速度。

PER

PER (Packed Encoding Rules) 最紧凑的编码规则,与 BER 不同的是,PER 不会发送 TLV 中的标签 T, 是应为消息中的组件的顺序是一致的,如果 TLV 中的值 V 是大小固定的,也不会发送长度 L. PER 同样使用的 ASN.1 定义之外的的信息来减少冗余的数据(V). 因此,PER 编码的消息更加紧凑,能够节省带宽.

XER,E-XER

XER (XML Encodind Rules), E-XER(Extended XER) 使用 XML 文本格式来进行编码, 格式为 <start-tag> value <end-tag>. XER 与 E-XER 的区别在于 E-XER 生成的默认编码更适合与 XSD(XML Schema Definition) 引擎进行信息交换. 相同的 ASN.1 数据可以使用多个编码规则, 这意味着来自于手机的 PER 编码的消息可以转换为 E-XER 编码, 用于在一个 web 浏览器中进行展示和操作

JER

JER (JSON Encoding Rules), 基于 JSON 格式的编码规则, 相比 XER, E-XER, 更加紧凑, 易于使用. 基于 RFC-7159 中的 JSON 规范.

通过JER,ASN.1用户可以轻松调试ASN.1协议并进行故障排除.

参考文献

[1] What is ASN1

[2] ASN.1简介及OpenSSL中ASN.1接口使用举例

[3] openssl 密钥生成和解析