16个版本 (1个稳定版本)
1.0.0 | 2024年6月1日 |
---|---|
1.0.0-alpha.4 | 2023年12月5日 |
0.9.0 | 2023年11月28日 |
0.2.0 | 2023年4月19日 |
#106 in 密码学
1,268 每月下载量
在 yacme 中使用
215KB
4.5K SLoC
JAWS: JSON Web令牌
JSON Web Tokens用于通过JavaScript对象表示法(JSON)对象发送已签名、经过身份验证和/或加密的数据。该包提供了一种强类型接口来创建和验证JWTs,它构建在RustCrypto生态系统之上。
JWT示例
这是一个从ACME标准(《RFC 8555》)中获取的JWT示例
{
"protected": base64url({
"alg": "ES256",
"jwk": {...
},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/new-account"
}),
"payload": base64url({
"termsOfServiceAgreed": true,
"contact": [
"mailto:cert-admin@example.org",
"mailto:admin@example.org"
]
}),
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
这个JWT表示使用NIST P-256曲线(《ES256
》)和ECDSA密钥签名的请求。有效载荷是一个包含对ACME服务器创建账户请求的JSON对象。
由于ACME账户通过加密密钥的公开部分进行识别,因此用于编码此JWT的公钥包含在JWT头部的jwk
中,即JSON Web Key(《JWK》)。
JSON Web Tokens包含一个受保护的头(上方的protected
),其中包含多个“注册”键(在本例中为alg
和jwk
),以及一组自定义键。有效载荷可以是任何JSON对象,尽管JWT标准定义了一组用于身份验证的声明,包括一些“注册”声明(声明在《ACME》)。
签名是通过计算受保护头和有效载荷的连接(均为base64url编码,并用点(.
)分隔)的ECDSA签名生成的。
强类型JWT
JAWS提供了一种创建和验证JWT的强类型接口。JAWS中的令牌必须在以下4种状态之一
Unsigned
: 未签名的令牌,没有签名。Signed
: 已签名的令牌。已签名的令牌不能被修改,因为那样可能会使签名无效。Verified
:一个已验证的令牌。已验证的令牌不能被修改,因为这可能会使签名无效。已验证的令牌不知道字段之间的关系(即jwk
头部可能代表与令牌完全无关的某些密钥)。Unverified
:一个已反序列化但尚未验证的令牌。
stateDiagram-v2
[*] --> Unverified : from JSON
Unverified --> Verified : Verify
Unsigned --> Signed : Sign
Signed --> Unverified: Unverify
Signed --> [*] : to JSON
示例用法
要创建简单的JWT,您需要提供一个加密密钥。此示例使用附录A.2中定义的RSA加密密钥RFC 7515,请勿重复使用!
此示例是从存储库中的examples/rfc7515a2.rs
复制的,并可以使用以下命令运行:cargo run --example rfc7515-a2
。
use jaws::Compact;
// JAWS provides JWT format for printing JWTs in a style similar to the example above,
// which is directly inspired by the way the ACME standard shows JWTs.
use jaws::JWTFormat;
// JAWS provides a single token type which is generic over the state of the token.
// The states are defined in the `state` module, and are used to track the
// signing and verification status.
use jaws::Token;
use jaws::key::DeserializeJWK;
// The unverified token state, used like `Token<.., Unverified<..>, ..>`.
// It is generic over the type of the custom header parameters.
use jaws::token::Unverified;
// JAWS provides type-safe support for JWT claims.
use jaws::{Claims, RegisteredClaims};
// We are going to use an RSA private key to sign our JWT, provided by
// the `rsa` crate in the RustCrypto suite.
use rsa::pkcs8::DecodePrivateKey;
// The signing algorithm we will use (`RS256`) relies on the SHA-256 hash
// function, so we get it here from the `sha2` crate in the RustCrypto suite.
use sha2::Sha256;
// Using serde_json allows us to quickly construct a serializable payload,
// but applications may want to instead define a struct and use serde to
// derive serialize and deserialize for added type safety.
use serde_json::json;
// Trait to convert a SigningKey into a VerifyingKey.
use signature::Keypair;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// This key is from RFC 7515, Appendix A.2. Provide your own key instead!
// The key here is stored as a PKCS#8 PEM file, but you can leverage
// RustCrypto to load a variety of other formats.
let key = rsa::RsaPrivateKey::from_pkcs8_pem(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/rfc7515a2.pem"
)))
.unwrap();
// We will sign the JWT with the RS256 algorithm: RSA with SHA-256.
let alg = rsa::pkcs1v15::SigningKey::<Sha256>::new(key);
// Claims can combine registered and custom fields. The claims object
// can be any type which implements [serde::Serialize].
let claims: Claims<serde_json::Value, (), String, (), ()> = Claims {
registered: RegisteredClaims {
subject: "1234567890".to_string().into(),
..Default::default()
},
claims: json!({
"name": "John Doe",
"admin": true,
}),
};
// Create a token with the default headers, and no custom headers.
// The unit type can be used here because it implements [serde::Serialize],
// but a custom type could be passed if we wanted to have custom header
// fields.
let mut token = Token::compact((), claims);
// We can modify the headers freely before signing the JWT. In this case,
// we provide the `typ` header, which is optional in the JWT spec.
*token.header_mut().r#type() = Some("JWT".to_string());
// We can also ask that some fields be derived from the signing key, for example,
// this will derive the JWK field in the header from the signing key.
token.header_mut().key().derived();
println!("=== Initial JWT ===");
// Initially the JWT has no defined signature:
println!("{}", token.formatted());
// Sign the token with the algorithm, and print the result.
let signed = token.sign::<_, rsa::pkcs1v15::Signature>(&alg).unwrap();
println!("=== Signed JWT ===");
println!("JWT:");
println!("{}", signed.formatted());
println!("Token: {}", signed.rendered().unwrap());
// We can't modify the token after signing it (that would change the signature)
// but we can access fields and read from them:
println!(
"Type: {:?}, Algorithm: {:?}",
signed.header().r#type(),
signed.header().algorithm(),
);
// We can also verify tokens.
let token: Token<Claims<serde_json::Value>, Unverified<()>, Compact> =
signed.rendered().unwrap().parse().unwrap();
println!("=== Parsed JWT ===");
// Unverified tokens can be printed for debugging, but there is deliberately
// no access to the payload, only to the header fields.
println!("JWT:");
println!("{}", token.formatted());
// We can use the JWK to verify that the token is signed with the correct key.
let hdr = token.header();
let jwk = hdr.key().unwrap();
let key = rsa::RsaPublicKey::from_jwk(jwk).unwrap();
assert_eq!(&key, alg.verifying_key().as_ref());
println!("=== Verification === ");
let alg: rsa::pkcs1v15::VerifyingKey<Sha256> = alg.verifying_key();
// We can't access the claims until we verify the token.
let verified = token
.verify::<_, jaws::algorithms::SignatureBytes>(&alg)
.unwrap();
println!("=== Verified JWT ===");
println!("JWT:");
println!("{}", verified.formatted());
println!(
"Payload: \n{}",
serde_json::to_string_pretty(&verified.payload()).unwrap()
);
Ok(())
}
哲学
在Rust生态系统中有很多JWT库,但我有以下几个原因想要创建一个
- 我想尝试构建一个强类型化的JWT库,并在尽可能多的方面将JWT生态系统建模为Rust类型系统。这意味着有时类型会变得过多(参见
jose
模块),但也意味着在这个库中难以(如果不是希望不可能的话)表示非法状态。 - 我想确保我有对已注册头部和已注册声明的强类型化支持,并以正确处理相互依赖的字段的方式来实现这一点,特别是
alg
头部值,它依赖于使用的加密密钥。大多数其他JWT库在运行时提供错误,如果将alg
头部设置为不兼容的值,但这个库根本不允许这样做。可以通过签名密钥派生其他头部值(例如x5t
、jwk
),以确保它们始终适用于使用的密钥。 - 我想广泛支持RustCrypto生态系统,并在可能的情况下,我已尝试在原生RustCrypto特性之上实现JWT。例如,
RS256
签名算法由rsa::pkcs1v15::SigningKey<sha2::SHA256>
类型表示,无需额外包装。 - 我还想提供一个强大的高级接口,使示例易于使用和跟踪。我希望尽管我上面的示例中有大量的注释,但仍然可以清楚地看出JAWS API的易于使用。
关于不安全代码
此库中没有用于主要JWT功能的不安全代码。唯一使用不安全代码的地方是在jaws::fmt
模块中,以提供高效的格式化方法。
然而,fmt
功能对于大多数功能不是必需的,而是主要用于调试JWT的内容。如果您担心不安全代码的使用,可以禁用fmt
功能以删除不安全代码。
依赖项
~11MB
~235K SLoC