16个版本 (1个稳定版本)

1.0.0 2024年6月1日
1.0.0-alpha.42023年12月5日
0.9.0 2023年11月28日
0.2.0 2023年4月19日

#106 in 密码学

Download history 14/week @ 2024-05-05 18/week @ 2024-05-12 198/week @ 2024-05-26 32/week @ 2024-06-02 8/week @ 2024-06-09 9/week @ 2024-06-16 7/week @ 2024-06-30 94/week @ 2024-07-07 248/week @ 2024-07-14 249/week @ 2024-07-21 411/week @ 2024-07-28 288/week @ 2024-08-04 315/week @ 2024-08-11

1,268 每月下载量
yacme 中使用

MIT 许可证

215KB
4.5K SLoC

JAWS: JSON Web令牌

crate Docs Build Status MIT licensed

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),其中包含多个“注册”键(在本例中为algjwk),以及一组自定义键。有效载荷可以是任何JSON对象,尽管JWT标准定义了一组用于身份验证的声明,包括一些“注册”声明(声明在《ACME》)。

签名是通过计算受保护头和有效载荷的连接(均为base64url编码,并用点(.)分隔)的ECDSA签名生成的。

强类型JWT

JAWS提供了一种创建和验证JWT的强类型接口。JAWS中的令牌必须在以下4种状态之一

  1. Unsigned: 未签名的令牌,没有签名。
  2. Signed: 已签名的令牌。已签名的令牌不能被修改,因为那样可能会使签名无效。
  3. Verified:一个已验证的令牌。已验证的令牌不能被修改,因为这可能会使签名无效。已验证的令牌不知道字段之间的关系(即jwk头部可能代表与令牌完全无关的某些密钥)。
  4. 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库,但我有以下几个原因想要创建一个

  1. 我想尝试构建一个强类型化的JWT库,并在尽可能多的方面将JWT生态系统建模为Rust类型系统。这意味着有时类型会变得过多(参见jose模块),但也意味着在这个库中难以(如果不是希望不可能的话)表示非法状态。
  2. 我想确保我有对已注册头部和已注册声明的强类型化支持,并以正确处理相互依赖的字段的方式来实现这一点,特别是alg头部值,它依赖于使用的加密密钥。大多数其他JWT库在运行时提供错误,如果将alg头部设置为不兼容的值,但这个库根本不允许这样做。可以通过签名密钥派生其他头部值(例如x5tjwk),以确保它们始终适用于使用的密钥。
  3. 我想广泛支持RustCrypto生态系统,并在可能的情况下,我已尝试在原生RustCrypto特性之上实现JWT。例如,RS256签名算法由rsa::pkcs1v15::SigningKey<sha2::SHA256>类型表示,无需额外包装。
  4. 我还想提供一个强大的高级接口,使示例易于使用和跟踪。我希望尽管我上面的示例中有大量的注释,但仍然可以清楚地看出JAWS API的易于使用。

关于不安全代码

此库中没有用于主要JWT功能的不安全代码。唯一使用不安全代码的地方是在jaws::fmt模块中,以提供高效的格式化方法。

然而,fmt功能对于大多数功能不是必需的,而是主要用于调试JWT的内容。如果您担心不安全代码的使用,可以禁用fmt功能以删除不安全代码。

依赖项

~11MB
~235K SLoC