JWT(JSON Web Token)
JWT是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式用于在网络应用环境间安全地传输信息。JWT可以被签名(JWS)或加密(JWE),也可以同时被签名和加密。一个典型的JWT由三部分组成,分别是头部(Header)、载荷(Payload)和签名/加密部分(Signature/Encryption)。这三部分通过.
分隔。
- Header:声明了使用的签名算法(如HS256或RS256)或加密方法。
- Payload:包含了声明(Claims),即你要传输的数据。
- Signature/Encryption:根据所选的算法对前面两部分进行签名或加密的结果。
JWS(JSON Web Signature)
JWS用于为JWT添加数字签名,以确保消息的完整性和来源的真实性。它允许接收者验证发送者的身份,并确认消息在传输过程中没有被篡改。JWS通常使用对称密钥(如HMAC)或非对称密钥(如RSA或ECDSA)来生成签名。
JWE(JSON Web Encryption)
JWE用于加密JWT的内容,以保护其机密性。它不仅加密了载荷,还可能包括加密内容加密密钥(CEK)和其他必要的元数据。JWE通常使用混合加密系统,其中对称加密用于加密消息体,而非对称加密用于加密对称密钥(CEK)。
关系总结
- JWT 是一种令牌格式,它可以被签名(JWS)或加密(JWE),或者两者兼有。
- JWS 和 JWE 是实现JWT的安全机制:
- JWS 确保JWT的完整性和真实性。
- JWE 确保JWT的机密性。
一般实践中有两种处理方式:先用JWS签名,再JWE加密;先JWE加密,再JWS签名。
在决定是先签名后加密还是先加密后签名时,主要取决于你想要实现的安全目标以及应用场景的具体需求。两种方法各有优缺点,适用于不同的安全场景。下面详细分析这两种方式:
1. 先签名后加密(Sign-then-Encrypt)
流程
- 签名:发送方使用自己的私钥对消息进行签名,生成数字签名。
- 加密:将签名后的消息及其签名一起加密,通常使用接收方的公钥。
优点
- 完整性保护:确保消息在整个传输过程中未被篡改。
- 不可否认性:因为签名是用发送方的私钥创建的,接收方可以验证消息确实来自声称的发送者,并且发送者不能否认自己发送的消息。
- 机密性:只有拥有相应私钥的接收方才能解密并验证签名,保证了消息的保密性和真实性。
缺点
- 隐私泄露风险:如果加密不完全覆盖签名信息,可能会导致签名信息暴露给中间人,尽管消息内容本身是加密的。
- 签名验证依赖于解密:接收方必须先解密才能验证签名,增加了处理复杂度。
发送方示例:
// 加载私钥用于JWS签名
RSAPrivateKey jwsPrivateKey = ... // 获取私钥的实际代码
// 创建并签名JWS对象
JWSObject jwsObject = new JWSObject(
new JWSHeader(JWSAlgorithm.PS256),
new Payload(paymentRequest)
);
jwsObject.sign(new RSASSASigner(jwsPrivateKey));
// 加载公钥证书用于JWE加密
RSAPublicKey jweRsaPubKey = ... // 获取公钥的实际代码
// 创建内容加密密钥(CEK)
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(enc.cekBitLength());
SecretKey cek = keyGenerator.generateKey();
// 创建和加密JWE对象,将签名后的JWS对象作为Payload
JWEObject jwe = new JWEObject(
new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP, EncryptionMethod.A256GCM).build(),
new Payload(jwsObject.serialize())
);
jwe.encrypt(new RSAEncrypter(jweRsaPubKey, cek));
String encryptedJwe = jwe.serialize();
接收方示例:
// 解析并解密JWE对象
JWEObject jwe = JWEObject.parse(encryptedJwe);
jwe.decrypt(new RSADecrypter(privateKey)); // 使用接收方的私钥解密
// 验证JWS签名
JWSObject jwsObject = JWSObject.parse(jwe.getPayload().toString());
boolean verified = jwsObject.verify(new RSASSAVerifier(publicKey)); // 使用发送方的公钥验证
if (verified) {
// 处理解密后的消息体
String payload = jwsObject.getPayload().toString();
} else {
throw new SecurityException("签名验证失败");
}
一般加密部分是使用对称加密对payload加密,然后使用非对称加密对加密的密钥进行加密。其中:
2. 先加密后签名(Encrypt-then-Sign)
流程
- 加密:发送方使用接收方的公钥加密消息。
- 签名:发送方用自己的私钥对加密后的消息进行签名。
优点
- 简化验证流程:接收方可以直接验证签名,而无需首先解密消息。
- 更好的隐私保护:整个加密后的消息都被签名,减少了签名信息泄露的风险。
缺点
- 缺乏不可否认性:由于签名是在加密之后进行的,理论上攻击者可以在不知道消息内容的情况下伪造签名,除非有额外的机制来绑定签名和加密操作。
- 签名无法证明原始消息:因为签名是对加密后的数据进行的,接收方只能验证加密后的数据是否未经篡改,但不能直接验证原始消息的完整性。
发送方示例:
// 加载平台的公钥用于签名
RSAPublicKey platformPublicKey = ... // 获取平台公钥的实际代码
// 加载自己的私钥用于加密
RSAPrivateKey myPrivateKey = ... // 获取自己私钥的实际代码
// 创建并加密JWE对象
JWEObject jwe = new JWEObject(
new JWEHeader(JWEAlgorithm.RSA_OAEP, EncryptionMethod.A256GCM),
new Payload(paymentRequest)
);
jwe.encrypt(new RSADecrypter(myPrivateKey)); // 注意这里是加密,使用私钥
// 创建并签名JWS对象
JWSObject jwsObject = new JWSObject(
new JWSHeader(JWSAlgorithm.PS256),
new Payload(jwe.serialize())
);
jwsObject.sign(new RSASSASigner(platformPublicKey)); // 注意这里是签名,使用公钥
接收方示例:
// 解析JWS对象
JWSObject jwsObject = JWSObject.parse(resp);
// 加载平台的私钥用于验证签名
RSAPrivateKey platformPrivateKey = ... // 获取平台私钥的实际代码
// 验证JWS签名
boolean verified = jwsObject.verify(new RSASSAVerifier(platformPrivateKey));
if (!verified) {
throw new SecurityException("签名验证失败");
}
// 获取解密后的JWE Payload
String jwePayload = jwsObject.getPayload().toString();
// 解析并解密JWE对象
JWEObject jwe = JWEObject.parse(jwePayload);
// 加载发送方的公钥用于解密
RSAPublicKey senderPublicKey = ... // 获取发送方公钥的实际代码
jwe.decrypt(new RSAEncrypter(senderPublicKey));
// 获取原始的消息体
String originalMessage = jwe.getPayload().toString();
推荐做法
根据学术研究和实际应用中的最佳实践,先签名后加密(Sign-then-Encrypt) 被认为是更安全的选择,尤其是在需要确保不可否认性和消息完整性的场景中。它提供了更强的保证,即消息确实是来自特定的发送者,并且在整个传输过程中没有被篡改。
然而,在某些特殊情况下,例如当隐私保护优先于不可否认性时,或者为了简化验证流程,可能会选择先加密后签名的方法。
总结
- 先签名后加密:提供更强的安全保证,包括不可否认性和消息完整性保护,适用于大多数需要严格安全控制的场景。
- 先加密后签名:简化了验证过程,提供了更好的隐私保护,但在不可否认性方面有所妥协。
选择哪种方式应基于你的具体需求和安全目标。如果你的应用场景强调消息的真实性和完整性,建议采用先签名后加密的方式;如果更关注隐私保护并且可以接受一定的不可否认性损失,则可以考虑先加密后签名。
# 生成私钥 (PEM 格式)
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
# 从私钥生成公钥 (PEM 格式)
openssl rsa -in private_key.pem -pubout -out public_key.pem
# 将私钥转换为 DER 格式(代码中需要 .der 文件)
openssl pkcs8 -topk8 -nocrypt -in private_key.pem -out private_key.der -outform DER
# 将公钥转换为 DER 格式(如果需要 .der 格式的公钥)
openssl rsa -in private_key.pem -pubout -outform DER -out public_key.der
# 从私钥生成自签名证书(有效期365天)
openssl req -new -x509 -key private_key.pem -out certificate.crt -days 365
# 将公钥转换为 .cer 格式(如果需要 .cer 格式的公钥)
openssl x509 -in public_key.pem -outform DER -out public_key.cer