feat: complete roadmap with comprehensive test coverage and adapters#2
Conversation
…s, and roadmap updates
- Updated CLAUDE.md to clarify the problem with secret scanners and added context on ecosystem gaps. - Modified Cargo.lock to include `serde_json` and `uselesskey-core` dependencies. - Revised README.md to improve clarity on outputs and added context on secret incidents. - Expanded BDD tests in rsa.feature to cover additional scenarios for deterministic and random RSA key generation. - Updated bdd.rs to support new scenarios and added handling for corrupted PEM and truncated DER. - Introduced new features for JWK generation and negative fixtures with corresponding feature files. - Added migration guide to assist users in transitioning from committed key fixtures to runtime generation.
- Implemented Ed25519 keypair generation with PKCS#8 support in the `uselesskey-ed25519` crate. - Added specifications for Ed25519 keys, including stable encoding for deterministic derivation. - Introduced X.509 certificate generation in the `uselesskey-x509` crate, supporting self-signed certificates and various key usages. - Implemented negative testing fixtures for X.509 certificates, allowing for the generation of intentionally invalid certificates for error handling. - Updated documentation for usage examples and negative fixture generation.
- Introduced new commands: `FeatureMatrix`, `NoBlob`, and `PublishCheck` in the xtask CLI. - Implemented `ci` function to run common CI tasks including feature matrix checks. - Added `run_feature_matrix` function to validate various feature combinations. - Created `publish_check` function to perform dry-run publishes for crates in dependency order. - Added HMAC secret fixture generation with tests for JWK and JWKS. - Implemented new crates: `uselesskey-hmac` and `uselesskey-jwk` for HMAC and JWK handling. - Updated documentation for release and requirements to reflect new features and commands.
- Introduced a new crate `uselesskey-jsonwebtoken` for integrating JWT functionality with uselesskey test fixtures. - Implemented extension traits for encoding and decoding JWTs using RSA, ECDSA, Ed25519, and HMAC keys. - Added examples demonstrating signing and verifying JWTs with different key types. - Updated README to reflect the new crate and its usage. - Enhanced existing examples to showcase JWK/JWKS generation and tempfile handling for key fixtures. - Added `uselesskey-jsonwebtoken` as a member in the workspace and included necessary dependencies.
The no-blob check was flagging .rs test files that contain PEM markers as strings (e.g., "-----BEGIN TEST KEY-----" in PEM corruption tests). These are source code, not committed fixtures. Also adds mutants.out/ to .gitignore.
…odebase - Created `timeout.txt` to track timeout-related mutants. - Created `unviable.txt` to document unviable mutants with specific code replacements using `Default::default()` for various structures across multiple crates.
…btoken in publish check
- Bump Rust edition from 2021 to 2024 in Cargo.toml and fuzz/Cargo.toml. - Update various dependencies to their latest versions, including: - thiserror to 2.0.18 - blake3 to 1.8.3 - dashmap to 6.1.0 - tempfile to 3.24.0 - proptest to 1.10.0 - rstest to 0.26.1 - anyhow to 1.0.101 - clap to 4.5.57 - cucumber to 0.22.1 - tokio to 1.49.0 - rsa to 0.9.10 - ed25519-dalek to 2.2.0 - serde to 1.0.228 - serde_json to 1.0.149 - base64 to 0.22.1 - Update Clippy MSRV to 1.92. - Adjust versions of dependencies in the `uselesskey-bdd` crate. - Refactor imports in various files for clarity and consistency. - Ensure safety in environment variable manipulation in doctests.
…d clean up test imports
…tures - Added `X509Chain` struct for generating a three-level X.509 certificate chain (root CA, intermediate CA, leaf). - Introduced `ChainSpec` struct to define specifications for certificate generation, including common names, validity periods, and RSA key sizes. - Created utility functions for deterministic base time and serial number generation. - Implemented negative testing scenarios for certificate chains, including hostname mismatches, unknown CAs, and expired certificates. - Added comprehensive tests for chain generation, PEM formatting, determinism, and negative cases.
…25519 dead_code) - x509: default validity 365d -> 3650d for self-signed + chain leaves - x509: reuse RSA keys across chain/self-signed variants (no variant in key labels) - x509: add tests for >=5y validity and key reuse across negative variants - ed25519: cfg_attr allow(dead_code) for public field when jwk is disabled
…y reuse in negative variants
…mpacted crates in plan
- Introduced a new crate `uselesskey-ring` to provide integration with the `ring` library for RSA, ECDSA, and Ed25519 key types. - Updated `Cargo.toml` files to include `uselesskey-ring` as a member and added necessary dependencies. - Enhanced README.md to document the new crate and its usage examples. - Implemented extension traits in `uselesskey-ring` for converting uselesskey fixtures into `ring` native signing key types. - Added convenience builders for `rustls::ServerConfig` and `rustls::ClientConfig` in `uselesskey-rustls`. - Implemented support for CRLs in X.509 chains, including tests for revoked certificates. - Updated xtask scripts to include `uselesskey-ring` in publish checks and dependency tracking.
…ryptographic libraries - Implement tests for deterministic factory creation and environment variable handling in `uselesskey-core`. - Add comprehensive tests for ECDSA keypair generation and validation in `uselesskey-ecdsa`. - Introduce tests for Ed25519 keypair functionality in `uselesskey-ed25519`. - Create README files for `uselesskey-jsonwebtoken`, `uselesskey-ring`, `uselesskey-rustcrypto`, and `uselesskey-rustls` with usage examples and feature descriptions. - Implement RustCrypto integration for keypair conversion in `uselesskey-rustcrypto`. - Add tests for HMAC functionality in `uselesskey-hmac`. - Ensure facade tests validate core items and re-export functionality across different key types.
- Moved downcast logic to a separate function `downcast_or_panic` for better readability and reuse. - Added tests for `downcast_or_panic` to ensure it correctly handles type mismatches and returns expected values. - Created a new test module `factory_tests` to encapsulate tests related to the `Factory` struct. - Improved the `random_seed` function to ensure it generates a seed of the expected length. - Updated existing tests to enhance clarity and maintainability, including assertions for cache behavior and debug output. - Added additional tests for X.509 certificate specifications and negative scenarios to ensure comprehensive coverage.
…add max iterations for TLS handshake
- Implement JWT integration tests covering signing/verification flows for RSA, ECDSA, Ed25519, and HMAC algorithms. - Include tests for JWT with JWKS for key lookup and cross-crate compatibility. - Add test utilities for deterministic key generation. - Create TLS integration tests for server/client configuration, mTLS scenarios, and cross-key type compatibility. - Ensure tests cover various key sizes and certificate chain structures. - Validate determinism in key and chain generation across different labels.
…ltiple test files - Added missing newlines at the end of files in `uselesskey-ring` and `uselesskey-rustcrypto` test utilities. - Improved readability by formatting long lines and aligning method calls in `crypto_backend_integration.rs`, `e2e_workflows.rs`, `jwt_integration.rs`, and `tls_integration.rs`. - Consolidated import statements in `e2e_workflows.rs` and `jwt_integration.rs` for better organization. - Enhanced consistency in JWT encoding and decoding error handling across various test modules. - Updated chain specifications in `chain_tests` for better clarity and maintainability. - Cleaned up unnecessary whitespace and improved overall code style in `xtask/src/main.rs`.
- Fix Ed25519 sign API calls in aws-lc-rs tests (returns Signature, not Result) - Fix typo in rustls adapter: private_key_derustls -> private_key_der_rustls - Fix integration tests: correct cfg feature names, remove nonexistent API calls - Remove accidentally committed bdd_output file - Add #![allow(unused)] to adapter test utility modules - Remove aws-lc-rs dependency from integration tests (requires NASM)
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR delivers comprehensive test coverage and adapter crate implementations for the uselesskey project. It adds integration tests for JWT/TLS/crypto backends, comprehensive BDD feature files covering JWKS/HMAC/X.509 edge cases, and test suites for ring, aws-lc-rs, and jsonwebtoken adapter crates. The PR also includes bug fixes for a rustls adapter typo and updates documentation to reflect the completed roadmap items.
Changes:
- Added 4 new integration test files (JWT, TLS, crypto backend, e2e workflows) with 1,904 lines of test code
- Added comprehensive test suites for adapter crates: ring (641 lines), aws-lc-rs (659 lines), jsonwebtoken (617 lines)
- Expanded BDD feature files with 230+ new scenarios covering RSA/HMAC variants, X.509 edge cases, JWKS rotation, and edge cases
- Updated documentation (roadmap, migration guide, architecture) and CHANGELOG to reflect completed features
Reviewed changes
Copilot reviewed 38 out of 39 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| xtask/src/receipt.rs | Added BddMatrixEntry struct and add_bdd_matrix method for tracking BDD test matrix results |
| xtask/src/plan.rs | Added logic to trigger feature matrix on BDD feature file changes |
| xtask/src/main.rs | Added bdd_matrix command for running BDD tests with feature sets |
| tests/tls_integration.rs | New TLS integration tests (346 lines) covering server/client config, mTLS, chains, determinism, negative fixtures |
| tests/testutil.rs | Shared deterministic factory utility for integration tests |
| tests/jwt_integration.rs | New JWT integration tests (553 lines) covering signing/verification for all key types, JWKS, cross-crate compatibility |
| tests/e2e_workflows.rs | New end-to-end workflow tests (671 lines) covering complete JWT/TLS/JWKS/chain creation workflows |
| tests/crypto_backend_integration.rs | New crypto backend integration tests (334 lines) for ring backend with RSA/ECDSA/Ed25519 |
| tests/Cargo.toml | New integration test crate configuration with feature flags for jwt/tls/crypto-backend/e2e tests |
| plans/*.md | New test architecture documentation and BDD coverage analysis |
| crates/uselesskey-ring/tests/ring_comprehensive.rs | Comprehensive ring adapter tests (641 lines) |
| crates/uselesskey-aws-lc-rs/tests/aws_lc_rs_comprehensive.rs | Comprehensive aws-lc-rs adapter tests (659 lines) |
| crates/uselesskey-jsonwebtoken/tests/jwt_comprehensive.rs | Comprehensive JWT adapter tests (617 lines) |
| crates/uselesskey-bdd/features/*.feature | Expanded BDD features with 230+ new scenarios |
| crates/uselesskey-rustls/src/lib.rs | Bug fix: corrected typo from private_key_derustls to private_key_der_rustls |
| docs/*.md | Updated roadmap, migration guide, and architecture documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| || paths.iter().any(|p| { | ||
| let p = normalize_path(p); | ||
| p.starts_with("crates/uselesskey/") | ||
| && (p.ends_with(".feature") && p.contains("features/")) |
There was a problem hiding this comment.
Logical error: The condition && (p.ends_with(".feature") && p.contains("features/")) will always be false because it's checking if a path that starts with "crates/uselesskey/" also ends with ".feature". This should use || (OR) instead of && (AND) to match either condition, or the first condition should be removed entirely since BDD feature files are in crates/uselesskey-bdd/features/, not crates/uselesskey/features/.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a3494924c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| default = ["uk-default"] | ||
| uk-default = ["uselesskey/default"] |
There was a problem hiding this comment.
Enable full key features for default BDD test builds
The new default feature set compiles uselesskey-bdd with only uselesskey/default (RSA), but crates/uselesskey-bdd/tests/bdd.rs imports ECDSA, Ed25519, HMAC, X.509, and JWK APIs unconditionally; as a result, the default BDD invocation (cargo xtask bdd, used in CI) will fail to compile whenever those symbols are not enabled. This regresses the BDD pipeline from a runnable default to a broken default configuration.
Useful? React with 👍 / 👎.
| p.starts_with("crates/uselesskey/") | ||
| && (p.ends_with(".feature") && p.contains("features/")) |
There was a problem hiding this comment.
Restore facade-change trigger for feature matrix checks
This condition now requires paths under crates/uselesskey/ to also be .feature files inside a features/ directory, which the facade crate does not have, so PR-scoped runs stop executing the feature matrix for normal facade Rust changes and only run it for Cargo metadata changes. That weakens regression detection for feature-gating issues in the public crate.
Useful? React with 👍 / 👎.
Summary of ChangesHello @EffortlessSteven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly advances the project's testing infrastructure and feature completeness. It introduces a broad array of new integration and BDD tests, ensuring that key generation, cryptographic operations, and adapter integrations are thoroughly validated. The update also refines development tooling and documentation, marking substantial progress on the project's roadmap by delivering comprehensive test coverage and robust adapter support. Highlights
Changelog
Ignored Files
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is an impressive pull request that massively expands the test coverage of the project. The addition of comprehensive integration tests for various adapters, new BDD scenarios for numerous features, and end-to-end workflow tests significantly improves the robustness and reliability of the codebase. The documentation updates and xtask enhancements are also valuable contributions. My review identified a few areas for improvement: a bug in a BDD test step that prevents a scenario from being tested correctly, some test code duplication that could affect maintainability, and a logic issue in the xtask CI plan. Overall, this is a high-quality contribution that moves the project forward significantly.
| world.x509_chain_sans.push(san.clone()); | ||
| let spec = X509Spec::self_signed(cn); | ||
| let new_x509 = fx.x509_self_signed(label, spec); |
There was a problem hiding this comment.
The add_san_to_cert function doesn't seem to be correctly applying the Subject Alternative Names (SANs) to the X509Spec. It collects SANs in world.x509_chain_sans but then creates a new X509Spec without using this list. This means the regenerated certificate will not contain the added SANs, and the corresponding test scenarios might not be testing the intended behavior. The spec should be updated with all collected SANs before regenerating the certificate.
| world.x509_chain_sans.push(san.clone()); | |
| let spec = X509Spec::self_signed(cn); | |
| let new_x509 = fx.x509_self_signed(label, spec); | |
| world.x509_chain_sans.push(san); | |
| let mut spec = X509Spec::self_signed(cn); | |
| spec.sans.extend(world.x509_chain_sans.iter().cloned()); | |
| spec.sans.sort(); | |
| spec.sans.dedup(); | |
| let new_x509 = fx.x509_self_signed(label, spec); |
| //! JWT Integration Tests | ||
| //! | ||
| //! Tests cross-crate JWT functionality: | ||
| //! - JWT signing/verification flows across different key types | ||
| //! - JWT with JWKS for key lookup | ||
| //! - Cross-crate compatibility between uselesskey-jsonwebtoken and other key crates | ||
| //! - JWT with different crypto backends (ring, aws-lc-rs, rustcrypto) | ||
|
|
||
| mod testutil; | ||
|
|
||
| use jsonwebtoken::jwk::Jwk; | ||
| use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation, decode, encode}; | ||
| use serde::{Deserialize, Serialize}; | ||
| use testutil::fx; | ||
| use uselesskey_ed25519::Ed25519Spec; | ||
| use uselesskey_jsonwebtoken::JwtKeyExt; | ||
| use uselesskey_jwk::JwksBuilder; | ||
| use uselesskey_rsa::{RsaFactoryExt, RsaSpec}; | ||
|
|
||
| #[derive(Debug, Serialize, Deserialize, PartialEq)] | ||
| struct JwtClaims { | ||
| sub: String, | ||
| exp: usize, | ||
| iat: usize, | ||
| iss: String, | ||
| } | ||
|
|
||
| impl JwtClaims { | ||
| fn new(sub: &str, exp: usize, iat: usize, iss: &str) -> Self { | ||
| Self { | ||
| sub: sub.to_string(), | ||
| exp, | ||
| iat, | ||
| iss: iss.to_string(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // RSA JWT Integration Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod rsa_jwt_tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_rsa_rs256_sign_verify() { | ||
| let fx = fx(); | ||
| let keypair = fx.rsa("test-rs256", RsaSpec::rs256()); | ||
|
|
||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "test-issuer"); | ||
| let header = Header::new(Algorithm::RS256); | ||
|
|
||
| let token = encode(&header, &claims, &keypair.encoding_key()) | ||
| .unwrap_or_else(|e| panic!("Failed to encode with RS256: {:?}", e)); | ||
|
|
||
| let validation = Validation::new(Algorithm::RS256); | ||
| let decoded = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| .unwrap_or_else(|e| panic!("Failed to decode with RS256: {:?}", e)); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rsa_custom_key_sizes() { | ||
| let test_cases = [(2048, "rsa-2048"), (3072, "rsa-3072"), (4096, "rsa-4096")]; | ||
|
|
||
| for (bits, label) in test_cases { | ||
| let fx = fx(); | ||
| let keypair = fx.rsa(label, RsaSpec::new(bits)); | ||
|
|
||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, label); | ||
| let header = Header::new(Algorithm::RS256); | ||
|
|
||
| let token = encode(&header, &claims, &keypair.encoding_key()) | ||
| .unwrap_or_else(|e| panic!("Failed to encode with {}-bit key: {:?}", bits, e)); | ||
|
|
||
| let validation = Validation::new(Algorithm::RS256); | ||
| let decoded = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| .unwrap_or_else(|e| panic!("Failed to decode with {}-bit key: {:?}", bits, e)); | ||
|
|
||
| assert_eq!( | ||
| decoded.claims, claims, | ||
| "Claims mismatch for {}-bit key", | ||
| bits | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_rsa_algorithm_mismatch_fails() { | ||
| let fx = fx(); | ||
| let keypair = fx.rsa("mismatch-test", RsaSpec::rs256()); | ||
|
|
||
| let claims = JwtClaims::new("user000", 9999999999, 1234567890, "mismatch-test"); | ||
| let header = Header::new(Algorithm::RS256); | ||
|
|
||
| let token = | ||
| encode(&header, &claims, &keypair.encoding_key()).expect("Failed to encode JWT"); | ||
|
|
||
| // Try to decode with wrong algorithm | ||
| let validation = Validation::new(Algorithm::RS384); | ||
| let result = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation); | ||
|
|
||
| assert!(result.is_err(), "Should fail with algorithm mismatch"); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // ECDSA JWT Integration Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod ecdsa_jwt_tests { | ||
| use super::*; | ||
| use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec}; | ||
|
|
||
| #[test] | ||
| fn test_jwt_ecdsa_es256_sign_verify() { | ||
| let fx = fx(); | ||
| let keypair = fx.ecdsa("es256-issuer", EcdsaSpec::Es256); | ||
|
|
||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "es256-issuer"); | ||
| let header = Header::new(Algorithm::ES256); | ||
|
|
||
| let token = encode(&header, &claims, &keypair.encoding_key()) | ||
| .expect("Failed to encode JWT with ES256"); | ||
|
|
||
| let validation = Validation::new(Algorithm::ES256); | ||
| let decoded = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with ES256"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_ecdsa_es384_sign_verify() { | ||
| let fx = fx(); | ||
| let keypair = fx.ecdsa("es384-issuer", EcdsaSpec::Es384); | ||
|
|
||
| let claims = JwtClaims::new("user456", 9999999999, 1234567890, "es384-issuer"); | ||
| let header = Header::new(Algorithm::ES384); | ||
|
|
||
| let token = encode(&header, &claims, &keypair.encoding_key()) | ||
| .expect("Failed to encode JWT with ES384"); | ||
|
|
||
| let validation = Validation::new(Algorithm::ES384); | ||
| let decoded = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with ES384"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_ecdsa_algorithm_mismatch_fails() { | ||
| let fx = fx(); | ||
| let keypair = fx.ecdsa("es256-mismatch", EcdsaSpec::Es256); | ||
|
|
||
| let claims = JwtClaims::new("user000", 9999999999, 1234567890, "es256-mismatch"); | ||
| let header = Header::new(Algorithm::ES256); | ||
|
|
||
| let token = | ||
| encode(&header, &claims, &keypair.encoding_key()).expect("Failed to encode JWT"); | ||
|
|
||
| // Try to decode with wrong algorithm | ||
| let validation = Validation::new(Algorithm::ES384); | ||
| let result = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation); | ||
|
|
||
| assert!(result.is_err(), "Should fail with algorithm mismatch"); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // Ed25519 JWT Integration Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod ed25519_jwt_tests { | ||
| use super::*; | ||
| use uselesskey_ed25519::Ed25519FactoryExt; | ||
|
|
||
| #[test] | ||
| fn test_jwt_ed25519_sign_verify() { | ||
| let fx = fx(); | ||
| let keypair = fx.ed25519("ed25519-issuer", Ed25519Spec::new()); | ||
|
|
||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "ed25519-issuer"); | ||
| let header = Header::new(Algorithm::EdDSA); | ||
|
|
||
| let token = encode(&header, &claims, &keypair.encoding_key()) | ||
| .expect("Failed to encode JWT with Ed25519"); | ||
|
|
||
| let validation = Validation::new(Algorithm::EdDSA); | ||
| let decoded = decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with Ed25519"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // HMAC JWT Integration Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod hmac_jwt_tests { | ||
| use super::*; | ||
| use uselesskey_hmac::{HmacFactoryExt, HmacSpec}; | ||
|
|
||
| #[test] | ||
| fn test_jwt_hmac_hs256_sign_verify() { | ||
| let fx = fx(); | ||
| let secret = fx.hmac("hs256-issuer", HmacSpec::Hs256); | ||
|
|
||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "hs256-issuer"); | ||
| let header = Header::new(Algorithm::HS256); | ||
|
|
||
| let token = encode(&header, &claims, &secret.encoding_key()) | ||
| .expect("Failed to encode JWT with HS256"); | ||
|
|
||
| let validation = Validation::new(Algorithm::HS256); | ||
| let decoded = decode::<JwtClaims>(&token, &secret.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with HS256"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_hmac_hs384_sign_verify() { | ||
| let fx = fx(); | ||
| let secret = fx.hmac("hs384-issuer", HmacSpec::Hs384); | ||
|
|
||
| let claims = JwtClaims::new("user456", 9999999999, 1234567890, "hs384-issuer"); | ||
| let header = Header::new(Algorithm::HS384); | ||
|
|
||
| let token = encode(&header, &claims, &secret.encoding_key()) | ||
| .expect("Failed to encode JWT with HS384"); | ||
|
|
||
| let validation = Validation::new(Algorithm::HS384); | ||
| let decoded = decode::<JwtClaims>(&token, &secret.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with HS384"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_hmac_hs512_sign_verify() { | ||
| let fx = fx(); | ||
| let secret = fx.hmac("hs512-issuer", HmacSpec::Hs512); | ||
|
|
||
| let claims = JwtClaims::new("user789", 9999999999, 1234567890, "hs512-issuer"); | ||
| let header = Header::new(Algorithm::HS512); | ||
|
|
||
| let token = encode(&header, &claims, &secret.encoding_key()) | ||
| .expect("Failed to encode JWT with HS512"); | ||
|
|
||
| let validation = Validation::new(Algorithm::HS512); | ||
| let decoded = decode::<JwtClaims>(&token, &secret.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT with HS512"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_hmac_wrong_secret_fails() { | ||
| let fx = fx(); | ||
| let secret1 = fx.hmac("secret1", HmacSpec::Hs256); | ||
| let secret2 = fx.hmac("secret2", HmacSpec::Hs256); | ||
|
|
||
| let claims = JwtClaims::new("user000", 9999999999, 1234567890, "secret1"); | ||
| let header = Header::new(Algorithm::HS256); | ||
|
|
||
| let token = | ||
| encode(&header, &claims, &secret1.encoding_key()).expect("Failed to encode JWT"); | ||
|
|
||
| // Try to decode with wrong secret | ||
| let validation = Validation::new(Algorithm::HS256); | ||
| let result = decode::<JwtClaims>(&token, &secret2.decoding_key(), &validation); | ||
|
|
||
| assert!(result.is_err(), "Should fail with wrong secret"); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // JWKS Integration Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod jwks_integration_tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_jwt_jwks_multi_key_lookup() { | ||
| let fx = fx(); | ||
|
|
||
| // Create multiple keys for different issuers | ||
| let issuer1 = fx.rsa("issuer1", RsaSpec::rs256()); | ||
| let issuer2 = fx.rsa("issuer2", RsaSpec::rs256()); | ||
| let issuer3 = fx.rsa("issuer3", RsaSpec::rs256()); | ||
|
|
||
| // Build JWKS with all keys | ||
| let jwks = JwksBuilder::new() | ||
| .add_public(issuer1.public_jwk()) | ||
| .add_public(issuer2.public_jwk()) | ||
| .add_public(issuer3.public_jwk()) | ||
| .build(); | ||
|
|
||
| // Verify all keys are in JWKS | ||
| assert_eq!(jwks.keys.len(), 3); | ||
|
|
||
| // Sign JWT with issuer2 | ||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "issuer2"); | ||
| let mut header = Header::new(Algorithm::RS256); | ||
| header.kid = Some(issuer2.kid().to_string()); | ||
|
|
||
| let token = | ||
| encode(&header, &claims, &issuer2.encoding_key()).expect("Failed to encode JWT"); | ||
|
|
||
| // Find correct key from JWKS by kid | ||
| let jwk = jwks | ||
| .keys | ||
| .iter() | ||
| .find(|k| k.kid() == issuer2.kid()) | ||
| .expect("Key not found in JWKS"); | ||
|
|
||
| // Convert AnyJwk to jsonwebtoken::Jwk | ||
| let jwk_value = serde_json::to_value(jwk).expect("Failed to serialize JWK"); | ||
| let jwk_json: Jwk = serde_json::from_value(jwk_value).expect("Failed to deserialize JWK"); | ||
|
|
||
| // Convert JWK to DecodingKey | ||
| let decoding_key = | ||
| DecodingKey::from_jwk(&jwk_json).expect("Failed to create DecodingKey from JWK"); | ||
|
|
||
| let validation = Validation::new(Algorithm::RS256); | ||
| let decoded = decode::<JwtClaims>(&token, &decoding_key, &validation) | ||
| .expect("Failed to decode JWT with JWKS key"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_jwt_jwks_key_rotation_scenario() { | ||
| let fx = fx(); | ||
|
|
||
| // Old key (still valid for verification) | ||
| let old_key = fx.rsa("old-key", RsaSpec::rs256()); | ||
|
|
||
| // New key (used for signing) | ||
| let new_key = fx.rsa("new-key", RsaSpec::rs256()); | ||
|
|
||
| // Build JWKS with both keys | ||
| let jwks = JwksBuilder::new() | ||
| .add_public(old_key.public_jwk()) | ||
| .add_public(new_key.public_jwk()) | ||
| .build(); | ||
|
|
||
| // Sign JWT with new key | ||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "new-key"); | ||
| let mut header = Header::new(Algorithm::RS256); | ||
| header.kid = Some(new_key.kid().to_string()); | ||
|
|
||
| let token = | ||
| encode(&header, &claims, &new_key.encoding_key()).expect("Failed to encode JWT"); | ||
|
|
||
| // Verify with JWKS (should find new key) | ||
| let jwk = jwks | ||
| .keys | ||
| .iter() | ||
| .find(|k| k.kid() == new_key.kid()) | ||
| .expect("New key not found in JWKS"); | ||
|
|
||
| // Convert AnyJwk to jsonwebtoken::Jwk | ||
| let jwk_value = serde_json::to_value(jwk).expect("Failed to serialize JWK"); | ||
| let jwk_json: Jwk = serde_json::from_value(jwk_value).expect("Failed to deserialize JWK"); | ||
|
|
||
| let decoding_key = | ||
| DecodingKey::from_jwk(&jwk_json).expect("Failed to create DecodingKey from JWK"); | ||
|
|
||
| let validation = Validation::new(Algorithm::RS256); | ||
| let decoded = decode::<JwtClaims>(&token, &decoding_key, &validation) | ||
| .expect("Failed to decode JWT with JWKS key"); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
|
|
||
| // Verify old key is still in JWKS for validating old tokens | ||
| assert!(jwks.keys.iter().any(|k| k.kid() == old_key.kid())); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // Cross-Crate Compatibility Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod cross_crate_compatibility_tests { | ||
| use super::*; | ||
| use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec}; | ||
| use uselesskey_ed25519::Ed25519FactoryExt; | ||
| use uselesskey_hmac::{HmacFactoryExt, HmacSpec}; | ||
|
|
||
| #[test] | ||
| fn test_all_key_types_produce_valid_jwts() { | ||
| let fx = fx(); | ||
|
|
||
| let test_cases = vec![ | ||
| ("rsa", Algorithm::RS256), | ||
| ("ecdsa", Algorithm::ES256), | ||
| ("ed25519", Algorithm::EdDSA), | ||
| ("hmac", Algorithm::HS256), | ||
| ]; | ||
|
|
||
| for (key_type, alg) in test_cases { | ||
| let claims = JwtClaims::new( | ||
| "user123", | ||
| 9999999999, | ||
| 1234567890, | ||
| &format!("{}-issuer", key_type), | ||
| ); | ||
| let header = Header::new(alg); | ||
|
|
||
| let token = match key_type { | ||
| "rsa" => { | ||
| let keypair = fx.rsa("rsa-issuer", RsaSpec::rs256()); | ||
| encode(&header, &claims, &keypair.encoding_key()) | ||
| } | ||
| "ecdsa" => { | ||
| let keypair = fx.ecdsa("ecdsa-issuer", EcdsaSpec::Es256); | ||
| encode(&header, &claims, &keypair.encoding_key()) | ||
| } | ||
| "ed25519" => { | ||
| let keypair = fx.ed25519("ed25519-issuer", Ed25519Spec::new()); | ||
| encode(&header, &claims, &keypair.encoding_key()) | ||
| } | ||
| "hmac" => { | ||
| let secret = fx.hmac("hmac-issuer", HmacSpec::Hs256); | ||
| encode(&header, &claims, &secret.encoding_key()) | ||
| } | ||
| _ => panic!("Unknown key type: {}", key_type), | ||
| } | ||
| .unwrap_or_else(|e| panic!("Failed to encode JWT with {}: {:?}", key_type, e)); | ||
|
|
||
| // Verify token can be decoded | ||
| let validation = Validation::new(alg); | ||
| let decoded = match key_type { | ||
| "rsa" => { | ||
| let keypair = fx.rsa("rsa-issuer", RsaSpec::rs256()); | ||
| decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| } | ||
| "ecdsa" => { | ||
| let keypair = fx.ecdsa("ecdsa-issuer", EcdsaSpec::Es256); | ||
| decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| } | ||
| "ed25519" => { | ||
| let keypair = fx.ed25519("ed25519-issuer", Ed25519Spec::new()); | ||
| decode::<JwtClaims>(&token, &keypair.decoding_key(), &validation) | ||
| } | ||
| "hmac" => { | ||
| let secret = fx.hmac("hmac-issuer", HmacSpec::Hs256); | ||
| decode::<JwtClaims>(&token, &secret.decoding_key(), &validation) | ||
| } | ||
| _ => panic!("Unknown key type: {}", key_type), | ||
| } | ||
| .unwrap_or_else(|e| panic!("Failed to decode JWT with {}: {:?}", key_type, e)); | ||
|
|
||
| assert_eq!(decoded.claims, claims); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_key_id_consistency_across_formats() { | ||
| let fx = fx(); | ||
| let keypair = fx.rsa("kid-test", RsaSpec::rs256()); | ||
|
|
||
| // Verify kid is consistent across different formats | ||
| let kid_from_keypair = keypair.kid(); | ||
| let public_jwk = keypair.public_jwk(); | ||
| let kid_from_jwk = public_jwk.kid(); | ||
| let public_jwks = keypair.public_jwks(); | ||
| let kid_from_jwks = public_jwks.keys[0].kid(); | ||
|
|
||
| assert_eq!(kid_from_keypair, kid_from_jwk); | ||
| assert_eq!(kid_from_keypair, kid_from_jwks); | ||
| } | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // Determinism Tests | ||
| // ========================================================================= | ||
|
|
||
| #[cfg(feature = "jwt")] | ||
| mod determinism_tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_deterministic_keys_produce_same_jwts() { | ||
| let fx1 = fx(); | ||
| let fx2 = fx(); | ||
|
|
||
| // Generate same key from same seed | ||
| let keypair1 = fx1.rsa("deterministic-jwt", RsaSpec::rs256()); | ||
| let keypair2 = fx2.rsa("deterministic-jwt", RsaSpec::rs256()); | ||
|
|
||
| // Sign same claims with both keys | ||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "deterministic-jwt"); | ||
| let header = Header::new(Algorithm::RS256); | ||
|
|
||
| let token1 = | ||
| encode(&header, &claims, &keypair1.encoding_key()).expect("Failed to encode JWT 1"); | ||
| let token2 = | ||
| encode(&header, &claims, &keypair2.encoding_key()).expect("Failed to encode JWT 2"); | ||
|
|
||
| // Tokens should be identical (same key + same claims) | ||
| assert_eq!( | ||
| token1, token2, | ||
| "Deterministic keys should produce identical JWTs" | ||
| ); | ||
|
|
||
| // Verify both tokens decode correctly | ||
| let validation = Validation::new(Algorithm::RS256); | ||
| let decoded1 = decode::<JwtClaims>(&token1, &keypair1.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT 1"); | ||
| let decoded2 = decode::<JwtClaims>(&token2, &keypair2.decoding_key(), &validation) | ||
| .expect("Failed to decode JWT 2"); | ||
|
|
||
| assert_eq!(decoded1.claims, claims); | ||
| assert_eq!(decoded2.claims, claims); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_different_labels_produce_different_jwts() { | ||
| let fx = fx(); | ||
|
|
||
| // Generate keys with different labels | ||
| let keypair1 = fx.rsa("label-1", RsaSpec::rs256()); | ||
| let keypair2 = fx.rsa("label-2", RsaSpec::rs256()); | ||
|
|
||
| // Sign same claims with both keys | ||
| let claims = JwtClaims::new("user123", 9999999999, 1234567890, "test"); | ||
| let header = Header::new(Algorithm::RS256); | ||
|
|
||
| let token1 = | ||
| encode(&header, &claims, &keypair1.encoding_key()).expect("Failed to encode JWT 1"); | ||
| let token2 = | ||
| encode(&header, &claims, &keypair2.encoding_key()).expect("Failed to encode JWT 2"); | ||
|
|
||
| // Tokens should be different (different keys) | ||
| assert_ne!( | ||
| token1, token2, | ||
| "Different labels should produce different JWTs" | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
This file appears to be a duplicate of crates/uselesskey-jsonwebtoken/tests/jwt_comprehensive.rs. This duplication can increase maintenance overhead. It would be beneficial to consolidate these tests into a single location, either within the uselesskey-jsonwebtoken crate's tests directory or in this workspace-level tests crate, to avoid having to update two files for any changes.
| || paths.iter().any(|p| { | ||
| let p = normalize_path(p); | ||
| p.starts_with("crates/uselesskey/") | ||
| && (p.ends_with(".feature") && p.contains("features/")) |
There was a problem hiding this comment.
The logic for determining when to run the feature matrix seems incorrect. The condition p.starts_with("crates/uselesskey/") && (p.ends_with(".feature") && p.contains("features/")) will likely never be true, as BDD .feature files are located in crates/uselesskey-bdd/features/, not crates/uselesskey/. This makes the condition confusing. If the intent is to run the feature matrix only when Cargo.toml or Cargo.lock files change (covered by cargo_changed), then the second part of the || expression could be removed for clarity.
Shipper 0.3.0-rc.2 emits the publish plan in alphabetical-looking order rather than topological order (EffortlessMetrics/shipper#173). With \`uselesskey-aws-lc-rs\` at position 1 and \`uselesskey-core\` at #2, shipper's linear walk fails immediately because cargo publish of aws-lc-rs requires uselesskey-ecdsa (#22) and other deps to already be on crates.io. The owner of shipper will fix this in the next patch. Until then, revert both \`release.yml\` and \`publish-retry.yml\` to use \`cargo xtask publish\`. The PUBLISH_CRATES order is correct after #565, and the test-helper dev-dep fix in #569 removes the cargo verification gap that the original cargo xtask path also tripped over. Keep \`.shipper/\` in \`.gitignore\` and the existing \`Quality gate\` / \`Publish preflight\` / \`Publish dry-run\` steps in preflight job — those are unaffected by the shipper revert and still useful. After this PR merges, retag v0.7.0 on the new main HEAD and let \`release.yml\` run end-to-end. shipper#173 stays open for the next release cycle.
Summary
Changes
tests/): JWT, TLS, crypto-backend, e2e workflow testsprivate_key_derustls→private_key_der_rustls), Ed25519 sign API in aws-lc-rs testsEvidence
-D warnings)Test Plan
cargo xtask clippypasses cleancargo xtask fmtpasses cleanUnknowns
x509_self_signed_with_keyAPI)