Release 0.3.0: PGP, tokens, tonic adapter, corruption fixtures, positioning refresh#4
Conversation
…y across all crates - Updated repository description in .github/settings.yml to reflect the purpose of the project. - Revised README.md to provide a clearer overview of the project. - Enhanced descriptions in Cargo.toml files for all crates to emphasize deterministic and random cryptographic test fixtures. - Adjusted keywords in Cargo.toml files to improve discoverability and relevance to cryptography. - Refactored tests in rsa_prop.rs and tls_integration.rs to utilize shared fixtures for efficiency and clarity.
- Introduced `uselesskey-token` crate for generating API key, bearer, and OAuth access token fixtures. - Implemented `TokenFactoryExt` trait to extend the core `Factory` for token generation. - Added deterministic and random modes for token generation, ensuring stable outputs for the same inputs. - Included tests to verify the stability and correctness of generated tokens. - Updated documentation to reflect the new token generation capabilities and examples.
- Introduced `uselesskey-pgp` crate for generating OpenPGP key material. - Implemented `PgpFactoryExt` trait to create PGP keys with various specifications. - Added tests to validate deterministic and random key generation. - Updated documentation for `uselesskey-pgp` with usage examples. feat: add tonic transport TLS adapters in `uselesskey-tonic` - Introduced `uselesskey-tonic` crate for converting X.509 fixtures to tonic transport types. - Implemented extension traits for `Identity`, `Certificate`, `ServerTlsConfig`, and `ClientTlsConfig`. - Added tests to ensure correct configuration of TLS settings. docs: update architecture and roadmap documentation - Documented the new `uselesskey-pgp` and `uselesskey-tonic` crates in architecture.md. - Updated roadmap.md to reflect the addition of OpenPGP and tonic features. - Revised release.md to include new crates in the publish order. test: enhance test coverage for new features - Added tests for PGP key generation and validation. - Included tests for tonic TLS configuration generation. - Ensured that changes in PGP and X.509 crates propagate correctly to dependent crates.
- Updated version numbers from 0.2.1 to 0.3.0 in Cargo.toml files for all crates. - Updated README.md files to reflect the new version in examples. - Updated CHANGELOG.md to include a new entry for version 0.3.0.
Summary by CodeRabbit
WalkthroughThis PR readies a 0.3.0 release: adds token, pgp, and tonic crates; introduces deterministic PEM/DER corruption helpers; adds no_std support and a cross-platform cache abstraction in core; bumps workspace crates to 0.3.0; and updates tests, docs, CI, and publish tooling. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Factory
participant CoreCache as CoreCache
participant Fixture as FixtureCrate
participant Negative as NegativeHelpers
Client->>Factory: fx.<type>(label, spec)
Factory->>CoreCache: get_or_init(domain,label,spec)
CoreCache->>CoreCache: check cache (DashMap or Mutex+BTreeMap)
alt cache hit
CoreCache-->>Factory: Arc<artifact>
else cache miss
CoreCache->>Fixture: generate artifact (deterministic seed)
Fixture-->>CoreCache: store artifact (Arc)
CoreCache-->>Factory: Arc<artifact>
end
Client->>Fixture: request deterministic corruption(artifact, variant)
Fixture->>Negative: corrupt_*_deterministic(artifact, variant)
Negative->>Negative: hash variant → selector → transform
Negative-->>Fixture: corrupted material
Fixture-->>Client: corrupted PEM/DER
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
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 expands the Highlights
Changelog
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 and substantial release PR for version 0.3.0. The introduction of PGP keys, token fixtures, a tonic adapter, and deterministic corruption helpers significantly expands the library's capabilities. The architectural improvements, particularly the addition of no_std support in uselesskey-core and the re-entrancy fix for the caching mechanism, are excellent and demonstrate careful design. The accompanying updates to documentation, tests, and examples are thorough and well-executed. The overall quality of the changes is very high. I have one minor suggestion regarding a potential typo in the changelog.
|
|
||
| ## [Unreleased] | ||
|
|
||
| ## [0.3.0] - 2026-02-17 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c706be2797
ℹ️ 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".
| let cached = cache_get(&self.inner.cache, &id) | ||
| .expect("uselesskey-core: cached artifact missing after insert"); |
There was a problem hiding this comment.
Avoid panicking after concurrent cache clear
After cache_insert_if_absent, this code does a second lookup and unconditionally expects it to exist. If another thread calls clear_cache() between those two operations, the entry can be removed and this path panics even though initialization succeeded. That makes shared Factory usage flaky under concurrency (for example, parallel tests that clear cache between phases). Returning the inserted/computed Arc directly (or having insert return the winner) would avoid this race.
Useful? React with 👍 / 👎.
| let span = chars - 1; | ||
| 1 + (u16::from_be_bytes([digest[1], digest[2]]) as usize % span) |
There was a problem hiding this comment.
Make deterministic PEM truncation always materially corrupt
The truncate length formula allows chars - 1, which for typical PEM input only removes the final newline. Because many PEM parsers accept files without a trailing newline, some deterministic "corrupt:*" variants can still parse successfully and stop being reliable negative fixtures. Tightening this bound so truncation removes at least one substantive PEM character would keep deterministic corruption consistently invalid.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Release 0.3.0 across the workspace, expanding fixture coverage (tokens + OpenPGP), adding deterministic corruption helpers, introducing a new tonic adapter, and hardening test/CI ergonomics (provider selection, caching, and performance).
Changes:
- Add new fixture crates
uselesskey-tokenanduselesskey-pgp, plus new adapter crateuselesskey-tonic. - Introduce deterministic PEM/DER corruption helpers and propagate deterministic corruption APIs into key/cert fixtures.
- Bump all workspace crate versions to
0.3.0, refresh docs/positioning, and update xtask/test infrastructure (BDD separation, path resolution, rustls provider handling).
Reviewed changes
Copilot reviewed 98 out of 99 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| xtask/src/plan.rs | Update impacted-crate dependency expansion for new crates/adapters. |
| xtask/src/main.rs | CI/test/publish workflow tweaks (feature matrix, exclude BDD from workspace tests, workspace-relative path helper, CWD test locking). |
| tests/tls_integration.rs | Share expensive X.509 fixtures across tests; ensure rustls provider installed. |
| tests/testutil.rs | Add helper to install ring as default rustls provider for --all-features runs. |
| tests/key_rotation.rs | Normalize JWT exp times; ensure rustls provider installed for workflows. |
| tests/jwt_integration.rs | Normalize JWT exp times for stability/portability. |
| tests/e2e_workflows.rs | Share expensive X.509 fixtures; normalize JWT exp times; ensure rustls provider installed. |
| tests/Cargo.toml | Version bump; ensure rustls is available for tests that install providers. |
| docs/roadmap.md | Mark delivered items for 0.3.0 (token/pgp/corruption/no_std/tonic). |
| docs/release.md | Update publish order and commands to include new crates/adapters. |
| docs/architecture.md | Document new crates/adapters and deterministic corruption support. |
| crates/uselesskey/tests/testutil.rs | Add shared deterministic factory for facade tests. |
| crates/uselesskey/tests/facade.rs | Add facade re-export tests for token/pgp; switch RSA test to shared deterministic factory. |
| crates/uselesskey/src/lib.rs | Re-export token/pgp APIs behind features; mention uselesskey-tonic. |
| crates/uselesskey/examples/negative_fixtures.rs | Gate example on x509 and provide helpful fallback main(). |
| crates/uselesskey/Cargo.toml | Bump to 0.3.0; add token/pgp deps and features; metadata refresh. |
| crates/uselesskey-x509/tests/x509_unit.rs | Update stable-bytes version prefix expectations (spec/chain). |
| crates/uselesskey-x509/src/spec.rs | Deduplicate SANs in stable-bytes; bump version prefix; add test. |
| crates/uselesskey-x509/src/negative.rs | Add deterministic corruption convenience wrappers for cert PEM/DER. |
| crates/uselesskey-x509/src/chain_spec.rs | Deduplicate SANs in stable-bytes; bump version prefix. |
| crates/uselesskey-x509/src/chain.rs | Deduplicate SANs when emitting rcgen SANs; adjust tests to deterministic factory. |
| crates/uselesskey-x509/src/cert.rs | Add deterministic corruption methods on X509Cert; add tests. |
| crates/uselesskey-x509/README.md | Refresh positioning and update examples to current API. |
| crates/uselesskey-x509/Cargo.toml | Bump to 0.3.0; metadata refresh. |
| crates/uselesskey-tonic/src/lib.rs | New tonic transport TLS adapter traits and tests. |
| crates/uselesskey-tonic/README.md | New crate README and usage example. |
| crates/uselesskey-tonic/Cargo.toml | New crate manifest (tonic dependency + x509 feature). |
| crates/uselesskey-token/src/token.rs | New token fixture implementation (api key/bearer/oauth-jwt-shape) + tests. |
| crates/uselesskey-token/src/spec.rs | Token spec model + stable-bytes encoding + tests. |
| crates/uselesskey-token/src/lib.rs | New crate root + exports. |
| crates/uselesskey-token/README.md | New crate README and usage example. |
| crates/uselesskey-token/Cargo.toml | New crate manifest. |
| crates/uselesskey-rustls/src/config.rs | Avoid reliance on global provider install in tests; prefer explicit provider in roundtrips. |
| crates/uselesskey-rustls/README.md | Refresh docs/examples for 0.3.0. |
| crates/uselesskey-rustls/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-rustcrypto/src/lib.rs | Use shared deterministic factory in tests for stability. |
| crates/uselesskey-rustcrypto/README.md | Refresh docs/examples for 0.3.0. |
| crates/uselesskey-rustcrypto/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-rsa/tests/testutil.rs | Add shared deterministic factory for RSA tests. |
| crates/uselesskey-rsa/tests/rsa_prop.rs | Expand deterministic corruption tests; reduce proptest cases; use shared deterministic factory in unit tests. |
| crates/uselesskey-rsa/src/keypair.rs | Add deterministic PEM/DER corruption helpers; make JWK alg reflect key size. |
| crates/uselesskey-rsa/README.md | Refresh docs/examples for 0.3.0 and current API names. |
| crates/uselesskey-rsa/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-ring/tests/ring_comprehensive.rs | Restructure imports/modules for clarity and deterministic testutil usage. |
| crates/uselesskey-ring/src/lib.rs | Use shared deterministic factory in tests for stability. |
| crates/uselesskey-ring/README.md | Refresh docs/examples for 0.3.0. |
| crates/uselesskey-ring/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-pgp/src/spec.rs | New OpenPGP spec model + stable-bytes encoding + tests. |
| crates/uselesskey-pgp/src/lib.rs | New crate root + exports. |
| crates/uselesskey-pgp/src/keypair.rs | New OpenPGP keypair fixture implementation with mismatch/corruption helpers + tests. |
| crates/uselesskey-pgp/README.md | New crate README and usage example. |
| crates/uselesskey-pgp/Cargo.toml | New crate manifest. |
| crates/uselesskey-jwk/README.md | Refresh crate README and add example. |
| crates/uselesskey-jwk/Cargo.toml | Bump to 0.3.0; metadata refresh. |
| crates/uselesskey-jsonwebtoken/tests/jwt_comprehensive.rs | Normalize JWT exp times for stability/portability. |
| crates/uselesskey-jsonwebtoken/src/lib.rs | Normalize exp in docs; use shared deterministic factory in tests. |
| crates/uselesskey-jsonwebtoken/README.md | Refresh docs/examples for 0.3.0. |
| crates/uselesskey-jsonwebtoken/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-hmac/tests/hmac_unit.rs | Tighten assertions and variable naming in debug/inequality tests. |
| crates/uselesskey-hmac/README.md | Refresh crate README and add example. |
| crates/uselesskey-hmac/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-ed25519/tests/keypair.rs | Add deterministic corruption tests and stronger assertions around corruption. |
| crates/uselesskey-ed25519/tests/jwk_private.rs | Add stronger JWK shape/byte-level assertions (kid/x/d). |
| crates/uselesskey-ed25519/src/keypair.rs | Add deterministic PEM/DER corruption helpers. |
| crates/uselesskey-ed25519/README.md | Refresh docs/examples for 0.3.0 and current API names. |
| crates/uselesskey-ed25519/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-ecdsa/tests/keypair.rs | Add deterministic corruption tests and stronger assertions around corruption. |
| crates/uselesskey-ecdsa/tests/jwk_private.rs | Add stronger JWK shape/byte-level assertions (kid/x/y/d). |
| crates/uselesskey-ecdsa/src/spec.rs | Add coordinate length helper used for JWK coordinate slicing/validation. |
| crates/uselesskey-ecdsa/src/keypair.rs | Add deterministic PEM/DER corruption helpers; enforce expected point length. |
| crates/uselesskey-ecdsa/README.md | Refresh docs/examples for 0.3.0 and current API names. |
| crates/uselesskey-ecdsa/Cargo.toml | Bump to 0.3.0; metadata refresh; update dep versions. |
| crates/uselesskey-core/tests/negative_prop.rs | Gate proptests on std for new no_std support. |
| crates/uselesskey-core/tests/factory_env.rs | Gate env-based tests on std for new no_std support. |
| crates/uselesskey-core/tests/core_prop.rs | Gate proptests on std for new no_std support. |
| crates/uselesskey-core/src/negative/pem.rs | Add deterministic corruption variant mapping; adjust for alloc/no_std; add tests. |
| crates/uselesskey-core/src/negative/mod.rs | Export deterministic corruption helpers and update module docs. |
| crates/uselesskey-core/src/negative/der.rs | Add deterministic DER corruption helper; adjust for alloc/no_std; add tests. |
| crates/uselesskey-core/src/lib.rs | Add no_std support via std default feature; gate sink/tests; add alloc. |
| crates/uselesskey-core/src/id.rs | Move to alloc/core; derive ordering for map keys; gate tests for std. |
| crates/uselesskey-core/src/factory_tests.rs | Add regression test for get_or_init re-entrancy safety. |
| crates/uselesskey-core/src/factory.rs | Make cache implementation std/no_std compatible; re-entrancy-safe get_or_init. |
| crates/uselesskey-core/src/error.rs | Move to alloc; gate IO variant/tests behind std. |
| crates/uselesskey-core/src/derive.rs | Avoid eprintln! in no_std builds for unknown derivation versions. |
| crates/uselesskey-core/README.md | Refresh positioning and document std vs no_std capabilities. |
| crates/uselesskey-core/Cargo.toml | Bump to 0.3.0; add std feature; make deps no_std-friendly; add spin mutex for no_std cache. |
| crates/uselesskey-bdd/Cargo.toml | Add facade feature mapping for pgp. |
| crates/uselesskey-aws-lc-rs/tests/aws_lc_rs_comprehensive.rs | Improve NASM gating (Windows vs non-Windows) for test skips. |
| crates/uselesskey-aws-lc-rs/src/lib.rs | Align cfg gating and tests with NASM availability; stabilize tests via shared deterministic factory. |
| crates/uselesskey-aws-lc-rs/build.rs | Add rerun directives; modernize cargo output keys; clarify warnings. |
| crates/uselesskey-aws-lc-rs/README.md | Refresh docs/examples for 0.3.0. |
| crates/uselesskey-aws-lc-rs/Cargo.toml | Bump to 0.3.0; metadata refresh; fix all feature to include native. |
| README.md | Refresh tagline/positioning; document new fixtures/adapters; bump version examples to 0.3. |
| Cargo.toml | Add new workspace members (token/pgp/tonic). |
| CLAUDE.md | Update project notes to reflect deterministic corruption + test workflow changes. |
| CHANGELOG.md | Add 0.3.0 release notes. |
| .github/settings.yml | Update repo description/topics to emphasize determinism and discoverability. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let payload = serde_json::json!({ | ||
| "iss": "uselesskey", | ||
| "sub": label, | ||
| "aud": "tests", | ||
| "scope": "fixture.read", | ||
| "jti": URL_SAFE_NO_PAD.encode(jti), | ||
| "exp": 4_102_444_800u64, | ||
| }); |
There was a problem hiding this comment.
exp is set to 4_102_444_800, which exceeds i32::MAX and can overflow in JWT parsers or downstream code that uses 32-bit timestamp types (the rest of the repo standardizes on 2_000_000_000 for this reason). Consider using a 32-bit-safe epoch seconds value here to keep the fixture broadly parseable.
| //! | ||
| //! Run with: cargo run --example negative_fixtures --features "rsa,x509" | ||
|
|
There was a problem hiding this comment.
The run instructions are inconsistent: the header says to enable rsa,x509, but the fallback main() instructs --features "x509". Since the x509 feature already enables rsa, update the example docs to match the actual required feature set.
| #[test] | ||
| #[cfg(feature = "token")] | ||
| fn token_reexport_works() { | ||
| use uselesskey::TokenFactoryExt; | ||
| use uselesskey::TokenSpec; | ||
|
|
||
| let fx = Factory::random(); | ||
| let token = fx.token("issuer", TokenSpec::api_key()); | ||
| assert!(token.value().starts_with("uk_test_")); | ||
| } | ||
|
|
||
| #[test] | ||
| #[cfg(feature = "pgp")] | ||
| fn pgp_reexport_works() { | ||
| use uselesskey::PgpFactoryExt; | ||
| use uselesskey::PgpSpec; | ||
|
|
||
| let fx = Factory::random(); | ||
| let key = fx.pgp("issuer", PgpSpec::ed25519()); | ||
| assert!( |
There was a problem hiding this comment.
These new facade re-export tests use Factory::random() instead of the shared deterministic testutil::fx(). For pgp in particular this can add avoidable keygen cost and reduces reproducibility across runs; consider using the shared deterministic factory here (as the RSA test does).
There was a problem hiding this comment.
Actionable comments posted: 41
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (17)
CLAUDE.md (2)
110-115:⚠️ Potential issue | 🟡 Minor
PgpFactoryExtis absent from the Extension Pattern section.
TokenFactoryExtwas correctly added at Line 114, butPgpFactoryExt— the parallel trait introduced byuselesskey-pgpin this same PR — is not listed.📝 Proposed addition
- `TokenFactoryExt` → `fx.token(label, spec)` - `X509FactoryExt` → `fx.x509(label, spec)` +- `PgpFactoryExt` → `fx.pgp(label, spec)`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` around lines 110 - 115, The Extension Pattern list in CLAUDE.md is missing the PgpFactoryExt entry; add a line mapping the new trait to the factory helper (e.g., `PgpFactoryExt` → `fx.pgp(label, spec)`) so the documentation shows the parallel API introduced by the uselesskey-pgp change; update the list near the existing `TokenFactoryExt` entry and use the exact symbol names `PgpFactoryExt` and `fx.pgp` to keep consistency.
80-96:⚠️ Potential issue | 🟡 Minor
uselesskey-pgpanduselesskey-tonicare missing from the workspace structure listing.Both crates are introduced in this PR (confirmed by the PR objectives and AI summary), but neither appears in the Architecture section. This leaves the guidance file incomplete for anyone — or any AI agent — working in this repo.
📝 Proposed additions
- **`crates/uselesskey-token`** - Token fixtures (API key, bearer, OAuth/JWT-shape) via `TokenFactoryExt` trait **`crates/uselesskey-x509`** - X.509 certificate fixtures via `X509FactoryExt` trait + **`crates/uselesskey-pgp`** - OpenPGP key fixtures via `PgpFactoryExt` trait **`crates/uselesskey-jsonwebtoken`** - Adapter: `jsonwebtoken` integration **`crates/uselesskey-rustls`** - Adapter: `rustls` / `rustls-pki-types` integration **`crates/uselesskey-ring`** - Adapter: `ring` integration **`crates/uselesskey-rustcrypto`** - Adapter: RustCrypto integration **`crates/uselesskey-aws-lc-rs`** - Adapter: `aws-lc-rs` integration + **`crates/uselesskey-tonic`** - Adapter: `tonic` TLS transport integration **`crates/uselesskey-bdd`** - Cucumber BDD tests🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CLAUDE.md` around lines 80 - 96, The Architecture section of CLAUDE.md omitted the two new crates introduced by the PR: `crates/uselesskey-pgp` and `crates/uselesskey-tonic`; update the listing block that enumerates workspace crates to include lines for **`crates/uselesskey-pgp`** (PGP-related fixtures/adapters) and **`crates/uselesskey-tonic`** (gRPC/tonic integration), matching the style of the other entries so the README accurately reflects the workspace; edit the Architecture section where the other `crates/uselesskey-*` entries are listed to add those two entries.crates/uselesskey-rustcrypto/src/lib.rs (1)
196-221: 🧹 Nitpick | 🔵 TrivialMigrate remaining
Factory::random()calls inecdsa_tests,ed25519_tests, andhmac_teststosuper::fx().
fx()was added to the parenttestsmodule and is already reachable viasuper::fx()from all three sub-modules, but lines 265, 281, 296, 304, 320, 342, 357, and 371 still callFactory::random(). This leaves the majority of the tests non-deterministic and inconsistent with thetest_rsa_sign_verifymigration.♻️ Proposed migration for `ecdsa_tests`, `ed25519_tests`, and `hmac_tests`
mod ecdsa_tests { use crate::RustCryptoEcdsaExt; - use uselesskey_core::Factory; use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec}; #[test] fn test_p256_sign_verify() { use p256::ecdsa::signature::{Signer, Verifier}; - let fx = Factory::random(); + let fx = super::super::fx(); ... } #[test] fn test_p384_sign_verify() { use p384::ecdsa::signature::{Signer, Verifier}; - let fx = Factory::random(); + let fx = super::super::fx(); ... } #[test] #[should_panic(expected = "expected P-384")] fn test_p384_on_p256_key_panics() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } #[test] #[should_panic(expected = "expected P-256")] fn test_p256_on_p384_key_panics() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } } mod ed25519_tests { use crate::RustCryptoEd25519Ext; use ed25519_dalek::{Signer, Verifier}; - use uselesskey_core::Factory; use uselesskey_ed25519::{Ed25519FactoryExt, Ed25519Spec}; #[test] fn test_ed25519_sign_verify() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } } mod hmac_tests { use crate::RustCryptoHmacExt; use hmac::Mac; - use uselesskey_core::Factory; use uselesskey_hmac::{HmacFactoryExt, HmacSpec}; #[test] fn test_hmac_sha256() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } #[test] fn test_hmac_sha384() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } #[test] fn test_hmac_sha512() { - let fx = Factory::random(); + let fx = super::super::fx(); ... } }Note: the path is
super::super::fx()because these are nested one level deeper (tests::ecdsa_tests, etc.) than wherefx()is defined (tests). Alternatively, move all sub-module tests up one level or re-exportfxinside each sub-module.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-rustcrypto/src/lib.rs` around lines 196 - 221, Replace non-deterministic Factory::random() calls in the nested test modules ecdsa_tests, ed25519_tests, and hmac_tests with the deterministic fx() helper added to the parent tests module; since these modules are nested one level deeper, call super::super::fx() wherever Factory::random() is used (e.g., inside the test functions in ecdsa_tests, ed25519_tests, and hmac_tests) so all tests use the fixed seed via fx() and become deterministic.xtask/src/plan.rs (1)
236-254: 🧹 Nitpick | 🔵 TrivialOptional: assert
"uselesskey-tonic"incore_change_expands_dependents.A core change transitively reaches
uselesskey-tonicviacore → x509 → tonic. The existing style of this test is not exhaustive on transitive dependents, but now that tonic is a first-class leaf in the graph, asserting it here would close the loop alongside the new token/pgp assertions.💡 Suggested addition
assert!(impacted.contains("uselesskey-x509")); assert!(impacted.contains("uselesskey")); assert!(impacted.contains("uselesskey-bdd")); + assert!(impacted.contains("uselesskey-tonic")); assert!(plan.run_bdd);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@xtask/src/plan.rs` around lines 236 - 254, The test core_change_expands_dependents in the xtask module doesn't assert that transitive dependent "uselesskey-tonic" is included; update the test (function core_change_expands_dependents) to add an assertion that plan.impacted_crates contains "uselesskey-tonic" so the transitive path core → x509 → tonic is covered; locate the test using build_plan(...) and impacted_crates and add an assert!(impacted.contains("uselesskey-tonic")) alongside the other impacted assertions.crates/uselesskey-rustls/src/config.rs (2)
376-414: 🧹 Nitpick | 🔵 TrivialConsider extracting the handshake loop into a shared helper.
The handshake-driving loop (client↔server byte shuffling, progress tracking, iteration cap) is duplicated verbatim across
tls_handshake_roundtrip,mtls_with_provider_roundtrip, andmtls_roundtrip. A small helper would eliminate ~60 lines of repetition:♻️ Suggested helper
fn drive_handshake( client: &mut rustls::ClientConnection, server: &mut rustls::ServerConnection, ) { let mut buf = Vec::new(); for iteration in 0..MAX_HANDSHAKE_ITERATIONS { let mut progress = false; buf.clear(); if client.wants_write() { client.write_tls(&mut buf).unwrap(); if !buf.is_empty() { server.read_tls(&mut &buf[..]).unwrap(); server.process_new_packets().unwrap(); progress = true; } } buf.clear(); if server.wants_write() { server.write_tls(&mut buf).unwrap(); if !buf.is_empty() { client.read_tls(&mut &buf[..]).unwrap(); client.process_new_packets().unwrap(); progress = true; } } if !progress { break; } assert!( iteration < MAX_HANDSHAKE_ITERATIONS - 1, "handshake did not complete within {} iterations", MAX_HANDSHAKE_ITERATIONS ); } assert!(!client.is_handshaking()); assert!(!server.is_handshaking()); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-rustls/src/config.rs` around lines 376 - 414, The handshake loop is duplicated in tls_handshake_roundtrip, mtls_with_provider_roundtrip, and mtls_roundtrip; extract it into a helper function (e.g., drive_handshake) that accepts &mut rustls::ClientConnection and &mut rustls::ServerConnection, performs the client↔server write/read/progress/iteration logic, and preserves the existing safety/assert checks (including the MAX_HANDSHAKE_ITERATIONS assertion and final !is_handshaking() asserts); then replace the duplicated loop in each of tls_handshake_roundtrip, mtls_with_provider_roundtrip, and mtls_roundtrip with a call to this helper to eliminate repetition.
205-227:⚠️ Potential issue | 🟠 MajorUse
WebPkiClientVerifier::builder_with_provider()inserver_config_mtls_rustls_with_provider.The
providerparameter passed toserver_config_mtls_rustls_with_provideris threaded throughServerConfig::builder_with_provider(provider)correctly, butWebPkiClientVerifier::builder(root_store.into()).build()at line 214 does not use the explicit provider.rustls 0.23 exposes
WebPkiClientVerifier::builder_with_provider(roots, provider: Arc<CryptoProvider>), which should be used here to ensure the client verifier respects the explicitly passed provider:Current code (line 214)
let client_verifier = rustls::server::WebPkiClientVerifier::builder(root_store.into()) .build() .expect("valid client verifier");Change to pass the
providerparameter:let client_verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(root_store.into(), provider.clone()) .build() .expect("valid client verifier");Without this fix, the client verifier silently falls back to the process-default
CryptoProvider, defeating the purpose of the explicit provider parameter.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-rustls/src/config.rs` around lines 205 - 227, The client verifier in server_config_mtls_rustls_with_provider is built with WebPkiClientVerifier::builder(...) and therefore ignores the explicit provider parameter; change the construction to call WebPkiClientVerifier::builder_with_provider(root_store.into(), provider.clone()) so the verifier uses the same Arc<CryptoProvider> passed into server_config_mtls_rustls_with_provider, then continue to .build().expect("valid client verifier") as before.crates/uselesskey-bdd/tests/bdd.rs (2)
2311-2335: 🛠️ Refactor suggestion | 🟠 Major
build_jwks_bothandbuild_jwks_containing_bothhave identical bodies — DRY violation.Unlike
build_jwks_all_threewhich correctly delegates tobuild_jwks_all, these two share no code despite having identical logic.build_jwks_containing_bothshould simply callbuild_jwks_both.♻️ Proposed fix
#[when(regex = r#"^I build a JWKS containing both keys with kids "([^"]+)" and "([^"]+)"$"#)] fn build_jwks_containing_both(world: &mut UselessWorld, kid1: String, kid2: String) { - assert_eq!(world.rsa_keys.len(), 2, "need 2 RSA keys"); - let mut jwk1 = world.rsa_keys[0].public_jwk(); - let mut jwk2 = world.rsa_keys[1].public_jwk(); - set_public_kid(&mut jwk1, &kid1); - set_public_kid(&mut jwk2, &kid2); - let mut builder = JwksBuilder::new(); - builder.push_public(jwk1); - builder.push_public(jwk2); - world.jwks_output_1 = Some(builder.build().to_value()); + build_jwks_both(world, kid1, kid2); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 2311 - 2335, Replace the duplicated body of build_jwks_containing_both by delegating to build_jwks_both: change build_jwks_containing_both to simply call build_jwks_both(world, kid1, kid2) (matching the same signature world: &mut UselessWorld, kid1: String, kid2: String) and return; remove the duplicate JWK construction code so only build_jwks_both contains the implementation.
30-30:⚠️ Potential issue | 🟡 Minor
#[derive(Debug)]onUselessWorldexposes raw private-key material.Fields
pkcs8_der_original,hmac_secret_1,hmac_secret_2,ed25519_pkcs8_der_original,ecdsa_pkcs8_der_original(and their_2siblings) areOption<Vec<u8>>containing raw PKCS#8 DER or HMAC secret bytes. When any cucumber step panics or a test runner prints world state, the full byte arrays would appear in logs.Implement a manual
Debugfor these fields, or introduce aRedactednewtype wrapper:🛡️ Proposed fix – redact sensitive fields from Debug
-#[derive(Default, Debug, World)] +#[derive(Default, World)] struct UselessWorld { ... } + +impl std::fmt::Debug for UselessWorld { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("UselessWorld") + .field("label", &self.label) + .field("pkcs8_der_original", &"<redacted>") + .field("hmac_secret_1", &"<redacted>") + .field("hmac_secret_2", &"<redacted>") + .field("ed25519_pkcs8_der_original", &"<redacted>") + .field("ecdsa_pkcs8_der_original", &"<redacted>") + // add non-sensitive fields as needed + .finish_non_exhaustive() + } +}As per coding guidelines, "Never print key material in Debug output to prevent key leakage" applies to
**/*.rs.Also applies to: 46-48, 66-67, 74-75, 88-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-bdd/tests/bdd.rs` at line 30, The current derive(Debug) on UselessWorld leaks raw key material (fields like pkcs8_der_original, hmac_secret_1/2, ed25519_pkcs8_der_original, ecdsa_pkcs8_der_original and their _2 variants) when the world is printed; remove the automatic #[derive(Debug)] from UselessWorld and either (a) implement a manual impl Debug for UselessWorld that redacts those sensitive Option<Vec<u8>> fields (showing e.g. "<redacted>" or lengths only) or (b) introduce a Redacted newtype wrapper used on those fields with its own Debug that does not reveal contents, then update the UselessWorld definition to use Redacted for the sensitive fields and keep non-sensitive fields printable; ensure the impl Debug or Redacted covers all listed sensitive names so test failures or panics won’t leak key bytes.xtask/src/main.rs (1)
158-168:⚠️ Potential issue | 🟡 Minor
--releasefor BDD suppressesdebug_assert!checks; confirm this is intentional.Release mode disables
debug_assert!and overflow checks. If BDD tests or their fixtures rely on these for correctness verification, failures may be silently swallowed. Additionally, release builds substantially increase CI compilation time. If the motivation is purely performance (e.g., crypto operations are too slow in debug mode), consider whether a profile withopt-level=1anddebug-assertions=truewould satisfy both needs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@xtask/src/main.rs` around lines 158 - 168, The bdd() function currently passes "--release" to cargo (see Command::new("cargo") args in bdd()), which disables debug_assert! and overflow checks and increases CI compile time; either remove the "--release" flag so tests run with debug assertions enabled, or replace it by using a custom cargo profile (e.g., add "--profile" and create a profile in Cargo.toml with opt-level=1 and debug-assertions=true) so BDD tests keep debug checks while improving performance.crates/uselesskey-core/tests/negative_prop.rs (1)
54-73: 🧹 Nitpick | 🔵 Trivial
fraction.max(1)is redundant; consider using2usize..10to avoid wasting proptest budget.Since
fraction in 1usize..10already guaranteesfraction >= 1, the.max(1)call is dead code. More importantly, whenfraction == 1,target_bytes == pem.len()causesprop_assume!(target_bytes < pem.len())to always discard the case, silently burning ~11 % of the proptest budget.✂️ Proposed fix
- fraction in 1usize..10 + fraction in 2usize..10 ) { - let target_bytes = pem.len() / fraction.max(1); + let target_bytes = pem.len() / fraction;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/tests/negative_prop.rs` around lines 54 - 73, The test corrupt_pem_truncate_produces_correct_length wastes proptest budget by allowing fraction == 1 and then masking it with .max(1); change the fraction generator from 1usize..10 to 2usize..10 and remove the unnecessary .max(1) call so target_bytes = pem.len() / fraction; keep the prop_assume!(target_bytes < pem.len()) and prop_assume!(target_bytes > 0) and the rest of the assertion using CorruptPem::Truncate unchanged.crates/uselesskey-x509/src/chain_spec.rs (1)
165-226: 🧹 Nitpick | 🔵 TrivialConsider adding a deduplication test for
ChainSpec::stable_bytes, parallel to the one added inspec.rs.
spec.rsaddstest_stable_bytes_deduplicates_sans;chain_spec.rsonly hastest_stable_bytes_san_order_independent. A sibling dedup test would close the coverage gap for the same change in this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-x509/src/chain_spec.rs` around lines 165 - 226, Add a new unit test in the tests module that mirrors spec.rs: create two ChainSpec instances (using ChainSpec::new and with_sans) where one has duplicate SAN entries (e.g., ["a.example.com", "a.example.com", "b.example.com"]) and the other has the duplicates removed (e.g., ["a.example.com", "b.example.com"]), then assert that their stable_bytes() values are equal; name the test test_stable_bytes_deduplicates_sans so it parallels the existing test_stable_bytes_san_order_independent and validates ChainSpec::stable_bytes deduplication behavior.crates/uselesskey-core/tests/factory_env.rs (2)
69-72:⚠️ Potential issue | 🟡 Minor
message.contains("invalid hex char")is brittle — it couples to internal library error text.If the underlying hex parser changes its error message, this assertion breaks the test without indicating a real regression. Consider asserting only on the error variant and
varfield, and optionally loggingmessagefor diagnostics.🔧 Proposed fix
match err { Error::InvalidSeed { var: got, message } => { assert_eq!(got, var); - assert!(message.contains("invalid hex char")); + // Verify the message is non-empty (content is library-internal). + assert!(!message.is_empty(), "expected a non-empty error message, got: {message}"); } other => panic!("unexpected error: {other:?}"), }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/tests/factory_env.rs` around lines 69 - 72, The test currently asserts on the internal error text with Error::InvalidSeed { var: got, message } and message.contains("invalid hex char"); remove the brittle string assertion and instead only assert the error variant and that the captured var equals the expected var (e.g., assert_eq!(got, var)); if you still want diagnostics, optionally print or debug-log the message rather than asserting its contents so the test no longer couples to the hex parser's error wording.
25-29: 🧹 Nitpick | 🔵 TrivialAdd a
// SAFETY:comment to theunsaferemove_varcall inDrop.
Dropcan be invoked during thread unwind while other threads are iterating the environment. Even though the variable name is unique, the call still satisfies theunsafecontract only if no concurrent mutation of the environment occurs. Document the invariant being upheld.✏️ Suggested safety comment
impl Drop for EnvGuard { fn drop(&mut self) { - unsafe { std::env::remove_var(&self.key) }; + // SAFETY: the variable name is unique to this test invocation + // (timestamp-derived), so no other thread observes or mutates it. + unsafe { std::env::remove_var(&self.key) }; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/tests/factory_env.rs` around lines 25 - 29, Add a SAFETY comment above the unsafe std::env::remove_var call in the EnvGuard Drop impl that documents the invariant ensuring no concurrent mutation of the process environment occurs when Drop runs; reference EnvGuard and its Drop implementation and state that the safety relies on tests creating unique variable names and ensuring EnvGuard is dropped only when no other threads may access or mutate std::env (e.g., tests run single-threaded or callers synchronize), so remove_var is safe to call during Drop.tests/tls_integration.rs (1)
215-228:⚠️ Potential issue | 🟡 Minor
test_deterministic_chains_produce_same_configsdoesn't actually verify determinism.The test name promises that the same seed + label produces identical chains, but the only assertion is that both
server_config.alpn_protocols.len() == 0— a trivially-zero property that proves nothing about cert content equality. The comment "cache hit on cloned Factory" describes the intended behavior but no assertion validates it.✅ Suggested stronger assertions
assert_eq!(server_config1.alpn_protocols.len(), 0); assert_eq!(server_config2.alpn_protocols.len(), 0); +// Actually assert determinism — same seed + label must produce byte-identical certs +assert_eq!(chain1.leaf_cert_pem(), chain2.leaf_cert_pem()); +assert_eq!(chain1.root_cert_pem(), chain2.root_cert_pem());🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/tls_integration.rs` around lines 215 - 228, The test test_deterministic_chains_produce_same_configs currently only checks alpn_protocols length and doesn't assert determinism; update it to actually compare the outputs from fx().x509_chain("deterministic", ChainSpec::new(...)) by asserting equality between chain1 and chain2 results: for example, compare the certificate bytes/DERs or the certificate list and private-key identifier produced by chain1 and chain2, and/or assert that server_config_rustls() produces equivalent configs (compare the certs presented by server_config_rustls(), or compute and compare fingerprints of the certs/keys) instead of the trivial alpn length checks so the test guarantees identical chains/configs.crates/uselesskey-core/tests/core_prop.rs (2)
30-53:⚠️ Potential issue | 🟡 Minor
deterministic_is_order_independent_for_cache_keysdoesn't actually exercise deterministic derivation.Both init closures return hardcoded values (
123u32,456u32) and ignore_rng. Afterclear_cache()the closures again return the same hardcoded values, so*a == *a2is trivially guaranteed regardless of seed, factory mode, or insertion order. The test would pass withFactory::random()too.To validate that BLAKE3-keyed derivation produces the same RNG state irrespective of initialization order, the closures need to consume the provided RNG:
🐛 Suggested fix — use the RNG in the closures
- let a = fx.get_or_init("domain:a", "label", &spec_bytes(1, 2), "good", |_rng| { - 123u32 - }); - let b = fx.get_or_init("domain:b", "label", &spec_bytes(3, 4), "good", |_rng| { - 456u32 - }); + let a = fx.get_or_init("domain:a", "label", &spec_bytes(1, 2), "good", |rng| { + use rand_core::RngCore; + rng.next_u32() + }); + let b = fx.get_or_init("domain:b", "label", &spec_bytes(3, 4), "good", |rng| { + use rand_core::RngCore; + rng.next_u32() + }); fx.clear_cache(); - let b2 = fx.get_or_init("domain:b", "label", &spec_bytes(3, 4), "good", |_rng| { - 456u32 - }); - let a2 = fx.get_or_init("domain:a", "label", &spec_bytes(1, 2), "good", |_rng| { - 123u32 - }); + let b2 = fx.get_or_init("domain:b", "label", &spec_bytes(3, 4), "good", |rng| { + use rand_core::RngCore; + rng.next_u32() + }); + let a2 = fx.get_or_init("domain:a", "label", &spec_bytes(1, 2), "good", |rng| { + use rand_core::RngCore; + rng.next_u32() + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/tests/core_prop.rs` around lines 30 - 53, The test deterministic_is_order_independent_for_cache_keys is currently using hardcoded return values in the get_or_init closures so it doesn't exercise the provided RNG; update the closures passed to Factory::deterministic().get_or_init (and the subsequent calls after fx.clear_cache()) to consume the supplied RNG parameter (the |_rng| argument) to produce the returned u32s (e.g., call a RNG method like gen::<u32>() / fill / next_u32 on the rng) so that the produced values depend on the deterministic RNG state and therefore legitimately verify order-independence of the BLAKE3-keyed derivation.
17-17: 🧹 Nitpick | 🔵 TrivialRemove the unused
_idbinding inderive_seed_for_test.
_idis created but never read —get_or_initconstructs its ownArtifactIdinternally from the same arguments. The_prefix silences the warning but leaves dead code and a misleading comment.♻️ Proposed cleanup
fn derive_seed_for_test(master: &Seed, domain: &'static str, label: &str, variant: &str) -> Seed { - let _id = ArtifactId::new(domain, label, &[0u8; 8], variant, DerivationVersion::V1); - // Use the factory to observe the derived seed indirectly via determinism. let fx = Factory::deterministic(*master);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/tests/core_prop.rs` at line 17, The binding _id in derive_seed_for_test is unused and should be removed to eliminate dead code; delete the line that calls ArtifactId::new and the unused _id variable so the test relies solely on the get_or_init call (which constructs its own ArtifactId from the same domain, label, &[0u8; 8], variant, DerivationVersion::V1), keeping derive_seed_for_test clean and warning-free.crates/uselesskey-aws-lc-rs/src/lib.rs (1)
162-227: 🧹 Nitpick | 🔵 TrivialECDSA and Ed25519 test modules still use
Factory::random()instead offx().The
fx()deterministic helper is defined (lines 126–133) and used byrsa_tests, butecdsa_tests(lines 171, 189) anded25519_tests(line 215) still useFactory::random(). This is inconsistent with the PR's goal of consistent factory initialization and makes those tests non-reproducible. It also explains the#[allow(dead_code)]annotations on lines 122/125.♻️ Proposed fix: use fx() in all test modules
mod ecdsa_tests { use crate::AwsLcRsEcdsaKeyPairExt; use aws_lc_rs::signature::{self, KeyPair}; - use uselesskey_core::Factory; use uselesskey_ecdsa::{EcdsaFactoryExt, EcdsaSpec}; #[test] fn test_ecdsa_p256_sign_verify() { - let fx = Factory::random(); + let fx = super::fx(); let ecdsa = fx.ecdsa("test", EcdsaSpec::es256());#[test] fn test_ecdsa_p384_sign_verify() { - let fx = Factory::random(); + let fx = super::fx(); let ecdsa = fx.ecdsa("test", EcdsaSpec::es384());mod ed25519_tests { use crate::AwsLcRsEd25519KeyPairExt; use aws_lc_rs::signature::{self, KeyPair}; - use uselesskey_core::Factory; use uselesskey_ed25519::{Ed25519FactoryExt, Ed25519Spec}; #[test] fn test_ed25519_sign_verify() { - let fx = Factory::random(); + let fx = super::fx(); let ed = fx.ed25519("test", Ed25519Spec::new());After this, the
#[allow(dead_code)]annotations on lines 122 and 125 can be removed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-aws-lc-rs/src/lib.rs` around lines 162 - 227, Tests in ecdsa_tests (test_ecdsa_p256_sign_verify, test_ecdsa_p384_sign_verify) and ed25519_tests (test_ed25519_sign_verify) use Factory::random() instead of the deterministic helper fx(); replace each Factory::random() call with fx() (the fx() helper defined earlier) so the tests use the deterministic factory, then remove the now-unnecessary #[allow(dead_code)] annotations referenced near fx() if desired.
| ecdsa = ["dep:uselesskey-ecdsa"] | ||
| ed25519 = ["dep:uselesskey-ed25519"] | ||
| all = ["rsa", "ecdsa", "ed25519"] | ||
| all = ["native", "rsa", "ecdsa", "ed25519"] |
There was a problem hiding this comment.
all feature now implicitly requires NASM on Windows.
Adding native to all means users enabling features = ["all"] will now pull in aws-lc-rs, which requires NASM on Windows (as noted in lines 29-31). This is a behavioral change from the previous version. Since this is a 0.x semver bump, it's technically fine, but consider documenting this in the CHANGELOG or migration notes so downstream users aren't surprised by build failures on Windows.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-aws-lc-rs/Cargo.toml` at line 37, The "all" feature in
Cargo.toml was changed to include "native", which implicitly pulls in aws-lc-rs
and introduces a Windows NASM build requirement; update the project
documentation (CHANGELOG or migration notes) to state that enabling features =
["all"] now enables the "native" feature and may require NASM on Windows,
mention aws-lc-rs as the dependent crate and the Windows build implication, and
include guidance for Windows users (install NASM or avoid the "native" feature)
so downstream users aren't surprised by build failures.
| #[cfg(any(not(windows), has_nasm))] | ||
| use testutil::fx; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && wc -l tests/aws_lc_rs_comprehensive.rsRepository: EffortlessMetrics/uselesskey
Length of output: 108
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && sed -n '1,50p' tests/aws_lc_rs_comprehensive.rsRepository: EffortlessMetrics/uselesskey
Length of output: 1672
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && rg -n "use testutil::fx|use uselesskey_core|use uselesskey_aws_lc_rs" tests/aws_lc_rs_comprehensive.rs | head -20Repository: EffortlessMetrics/uselesskey
Length of output: 164
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && rg -n "\bfx\b|\bFactory\b|\bSeed\b" tests/aws_lc_rs_comprehensive.rs | head -30Repository: EffortlessMetrics/uselesskey
Length of output: 1701
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && rg -n "^mod \w+" tests/aws_lc_rs_comprehensive.rsRepository: EffortlessMetrics/uselesskey
Length of output: 259
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && sed -n '28,35p' tests/aws_lc_rs_comprehensive.rsRepository: EffortlessMetrics/uselesskey
Length of output: 297
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && rg -B2 "^mod " tests/aws_lc_rs_comprehensive.rs | head -40Repository: EffortlessMetrics/uselesskey
Length of output: 681
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && sed -n '462,550p' tests/aws_lc_rs_comprehensive.rs | head -50Repository: EffortlessMetrics/uselesskey
Length of output: 1566
🏁 Script executed:
cd crates/uselesskey-aws-lc-rs && sed -n '534,600p' tests/aws_lc_rs_comprehensive.rs | head -50Repository: EffortlessMetrics/uselesskey
Length of output: 1680
Import guards must match their usage sites to avoid unused-import warnings.
Both use testutil::fx (line 20) and use uselesskey_core::{Factory, Seed} (line 26) are guarded only by the platform predicate any(not(windows), has_nasm), but all their usages occur within the rsa_aws_lc_rs_tests, ecdsa_aws_lc_rs_tests, and ed25519_aws_lc_rs_tests modules—each of which requires feature = "native". On a non-Windows platform with the native feature disabled, these imports compile while all their consuming modules are compiled out, triggering unused import warnings.
🐛 Proposed fix
-#[cfg(any(not(windows), has_nasm))]
+#[cfg(all(feature = "native", any(not(windows), has_nasm)))]
use testutil::fx;
-#[cfg(any(not(windows), has_nasm))]
+#[cfg(all(feature = "native", any(not(windows), has_nasm)))]
use uselesskey_core::{Factory, Seed};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[cfg(any(not(windows), has_nasm))] | |
| use testutil::fx; | |
| #[cfg(all(feature = "native", any(not(windows), has_nasm)))] | |
| use testutil::fx; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-aws-lc-rs/tests/aws_lc_rs_comprehensive.rs` around lines 20
- 21, The imports guarded by #[cfg(any(not(windows), has_nasm))] (specifically
use testutil::fx and use uselesskey_core::{Factory, Seed}) are still compiled
when the native feature is disabled which causes unused-import warnings because
their consumers live inside modules gated by feature = "native"; change the
import cfg to match the consumers (e.g. #[cfg(all(feature = "native",
any(not(windows), has_nasm)))]) so the imports are only included when the
rsa_aws_lc_rs_tests, ecdsa_aws_lc_rs_tests, and ed25519_aws_lc_rs_tests modules
are compiled.
| #[then("the leaf private key should be a valid PKCS#8 key")] | ||
| fn leaf_private_key_is_valid_pkcs8(world: &mut UselessWorld) { | ||
| use rsa::pkcs8::DecodePrivateKey; | ||
|
|
||
| let chain = world.x509_chain.as_ref().expect("x509_chain not set"); | ||
| rsa::RsaPrivateKey::from_pkcs8_der(chain.leaf_private_key_pkcs8_der()) | ||
| .expect("leaf private key should parse as PKCS#8"); | ||
| } | ||
|
|
||
| #[then("the leaf private key should match the leaf certificate")] | ||
| fn leaf_private_key_matches_leaf_cert(world: &mut UselessWorld) { | ||
| use rsa::pkcs8::DecodePrivateKey; | ||
| use rsa::pkcs8::DecodePublicKey; | ||
| use rsa::traits::PublicKeyParts; | ||
|
|
||
| let chain = world.x509_chain.as_ref().expect("x509_chain not set"); | ||
| let private_key = | ||
| rsa::RsaPrivateKey::from_pkcs8_der(chain.leaf_private_key_pkcs8_der()).expect("parse key"); | ||
| let (_, cert) = x509_parser::parse_x509_certificate(chain.leaf_cert_der()).expect("parse cert"); | ||
| let cert_public = | ||
| rsa::RsaPublicKey::from_public_key_der(cert.public_key().raw).expect("parse cert SPKI"); | ||
| let private_public = private_key.to_public_key(); | ||
|
|
||
| assert_eq!(private_public.n(), cert_public.n(), "modulus should match"); | ||
| assert_eq!(private_public.e(), cert_public.e(), "exponent should match"); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find ChainSpec definition and usage
rg -n "struct ChainSpec|impl.*ChainSpec|ChainSpec::new" crates/uselesskey-x509/src/ crates/uselesskey-bdd/Repository: EffortlessMetrics/uselesskey
Length of output: 3530
🏁 Script executed:
# Check what leaf key types are generated
rg -n "leaf.*key|leaf_cert|leaf_private" crates/uselesskey-x509/src/ -A 2 -B 2Repository: EffortlessMetrics/uselesskey
Length of output: 14932
🏁 Script executed:
# Check x509_chain structure definition
rg -n "struct.*X509Chain|x509_chain.*:" crates/ -A 5Repository: EffortlessMetrics/uselesskey
Length of output: 15521
🏁 Script executed:
# Verify pkcs8 crate availability
rg -n "pkcs8" crates/uselesskey-x509/Cargo.toml crates/uselesskey-bdd/Cargo.tomlRepository: EffortlessMetrics/uselesskey
Length of output: 279
Both leaf_private_key_is_valid_pkcs8 and leaf_private_key_matches_leaf_cert silently assume RSA leaf keys.
Currently ChainSpec::new() only generates RSA keys. However, the codebase architecture (separate crates for RSA/ECDSA/Ed25519) suggests multi-algorithm leaf key support may be added. If so, both steps will fail with an opaque DER-parse error rather than a meaningful test message.
♻️ Suggested improvement – algorithm-agnostic parse-and-compare
#[then("the leaf private key should be a valid PKCS#8 key")]
fn leaf_private_key_is_valid_pkcs8(world: &mut UselessWorld) {
let chain = world.x509_chain.as_ref().expect("x509_chain not set");
- use rsa::pkcs8::DecodePrivateKey;
- rsa::RsaPrivateKey::from_pkcs8_der(chain.leaf_private_key_pkcs8_der())
- .expect("leaf private key should parse as PKCS#8");
+ // Accepts any PKCS#8-encoded key type.
+ pkcs8::PrivateKeyInfo::try_from(chain.leaf_private_key_pkcs8_der())
+ .expect("leaf private key should be valid PKCS#8");
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 1882 - 1907, The two test
steps leaf_private_key_is_valid_pkcs8 and leaf_private_key_matches_leaf_cert
assume RSA and call rsa::RsaPrivateKey::from_pkcs8_der etc., which will produce
opaque DER errors if non-RSA keys are used; update both functions to detect the
certificate public-key algorithm from cert.public_key().algorithm (or SPKI OID)
and then parse/validate the private key and compare pubkeys in an
algorithm-specific branch for RSA, ECDSA (parse PKCS#8 to an EC private key and
compare curve+public point), and Ed25519 (parse to ed25519 private key and
compare public key bytes), returning clear assertion messages on unsupported or
mismatched algorithms; keep the existing RSA code path for
RsaPublicKey/RsaPrivateKey and add explicit, informative expect/assert messages
naming the detected algorithm and the failure reason.
| #[when("I generate the same keys again in reverse order")] | ||
| fn gen_same_keys_reverse_again(world: &mut UselessWorld) { | ||
| // Snapshot PEMs before regenerating. | ||
| world.rsa_pems_before = world | ||
| .rsa_keys | ||
| .iter() | ||
| .map(|k| k.private_key_pkcs8_pem().to_string()) | ||
| .collect(); | ||
| if let Some(k) = &world.ecdsa { | ||
| world.ecdsa_pems_before = vec![k.private_key_pkcs8_pem().to_string()]; | ||
| } | ||
| if let Some(k) = &world.ed25519 { | ||
| world.ed25519_pems_before = vec![k.private_key_pkcs8_pem().to_string()]; | ||
| } | ||
|
|
||
| let fx = world.factory.as_ref().expect("factory not set"); | ||
| // Regenerate in reverse order, preserving the labels used by cross_key.feature. | ||
| let ed25519 = fx.ed25519("isolation-ed25519", Ed25519Spec::new()); | ||
| let ecdsa = fx.ecdsa("isolation-ecdsa", EcdsaSpec::es256()); | ||
| let rsa = fx.rsa("isolation-rsa", RsaSpec::rs256()); | ||
|
|
||
| world.rsa_keys.clear(); | ||
| world.rsa_keys.push(rsa.clone()); | ||
| world.rsa = Some(rsa); | ||
| world.ecdsa = Some(ecdsa); | ||
| world.ed25519 = Some(ed25519); | ||
| } |
There was a problem hiding this comment.
gen_same_keys_reverse_again silently requires "isolation-*" labels already in world.rsa_keys to produce a valid snapshot comparison.
This step snapshots world.rsa_pems_before from the current world.rsa_keys, then regenerates with hardcoded "isolation-rsa", "isolation-ecdsa", and "isolation-ed25519" labels. In a deterministic factory, different label strings → different key bytes. If this step is invoked after gen_same_keys_reverse (which populates world.rsa_keys with the "rsa" label), the snapshot is of the "rsa" key but the regenerated key uses "isolation-rsa" — regenerated_keys_identical would then fail spuriously.
The inline comment (// preserving the labels used by cross_key.feature) partially explains the design, but the step name "I generate the same keys again in reverse order" doesn't advertise this label dependency.
Consider one of:
- Renaming to make the isolation scope explicit (e.g.,
"I generate the same isolation keys again in reverse order"), or - Asserting at the start of the step that the world already has isolation-labelled keys, or
- Deriving the labels to regenerate from
world.rsa_keys' existing labels instead of hardcoding them.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 3106 - 3132, The step
gen_same_keys_reverse_again assumes the preexisting keys use "isolation-*"
labels but hardcodes new labels ("isolation-rsa", "isolation-ecdsa",
"isolation-ed25519"), causing spurious mismatches if world.rsa_keys were
previously created with different labels; fix by deriving the labels from the
existing world state (e.g., read the label(s) from world.rsa_keys and from
world.ecdsa/world.ed25519 if present) and pass those derived labels to the
factory calls (fx.rsa(...), fx.ecdsa(...), fx.ed25519(...)) so regenerated keys
match the snapshot rather than using hardcoded strings.
| fn fx() -> uselesskey_core::Factory { | ||
| testutil::install_rustls_ring_provider(); | ||
| testutil::fx() | ||
| } |
There was a problem hiding this comment.
fn fx() lacks a cfg guard, causing a compile error when no relevant features are enabled.
testutil::install_rustls_ring_provider is only compiled under cfg(any(feature = "tls", feature = "e2e", feature = "key-rotation")), but the wrapping fn fx() has no such guard. Any invocation of cargo test without those features will fail with E0425: cannot find function install_rustls_ring_provider.
🔧 Proposed fix
+#[cfg(any(feature = "tls", feature = "e2e", feature = "key-rotation"))]
fn fx() -> uselesskey_core::Factory {
testutil::install_rustls_ring_provider();
testutil::fx()
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn fx() -> uselesskey_core::Factory { | |
| testutil::install_rustls_ring_provider(); | |
| testutil::fx() | |
| } | |
| #[cfg(any(feature = "tls", feature = "e2e", feature = "key-rotation"))] | |
| fn fx() -> uselesskey_core::Factory { | |
| testutil::install_rustls_ring_provider(); | |
| testutil::fx() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/key_rotation.rs` around lines 31 - 34, The helper function fx() calls
testutil::install_rustls_ring_provider which is only available under
cfg(any(feature = "tls", feature = "e2e", feature = "key-rotation")), so add the
same cfg to fx() (or split into two fx variants) to avoid unresolved symbol
errors: either annotate fn fx() with #[cfg(any(feature = "tls", feature = "e2e",
feature = "key-rotation"))] so it and its call to
testutil::install_rustls_ring_provider are only compiled when those features are
enabled, or keep an unguarded fx() that simply delegates to testutil::fx() and
create a guarded fx() that performs install_rustls_ring_provider before calling
testutil::fx(); reference the function names fx and
testutil::install_rustls_ring_provider when making the change.
| #[test] | ||
| fn test_chain_with_sans() { | ||
| let fx = fx(); | ||
|
|
||
| let chain_spec = ChainSpec::new("sans.example.com").with_sans(vec![ | ||
| "localhost".to_string(), | ||
| "127.0.0.1".to_string(), | ||
| "*.example.com".to_string(), | ||
| ]); | ||
| let chain = fx.x509_chain("sans-test", chain_spec); | ||
|
|
||
| // Build server config | ||
| // shared_chain() already includes SANs | ||
| let chain = shared_chain(); | ||
| let server_config = chain.server_config_rustls(); | ||
|
|
||
| // Verify config was created successfully | ||
| assert_eq!(server_config.alpn_protocols.len(), 0); | ||
| } |
There was a problem hiding this comment.
test_chain_with_sans doesn't assert that SANs are actually present.
The test comment acknowledges that the shared chain includes SANs, but only asserts alpn_protocols.len() == 0. If the goal is to confirm SAN handling, at minimum verify the PEM contains the expected SAN data (or that the server config builds without error for a SAN-required use case).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/tls_integration.rs` around lines 197 - 203, test_chain_with_sans
currently only checks alpn_protocols but doesn't verify SANs; update the test to
assert the SANs are actually present by inspecting the shared_chain() result:
call shared_chain(), then either check the PEM/DER contents for the expected SAN
entry (e.g. look for "DNS:localhost" or the hostname you expect) or access the
chain structure (e.g. chain.leaf or chain.certs) and assert the subjectAltName
list contains the expected DNS entries, keeping the existing
server_config_rustls() assertion intact; use the unique symbols
test_chain_with_sans, shared_chain, server_config_rustls, and chain (chain.pem /
chain.leaf / chain.certs) to locate where to add the assertion.
| fn workspace_path(rel: &str) -> PathBuf { | ||
| let cwd_rel = PathBuf::from(rel); | ||
| if cwd_rel.exists() { | ||
| return cwd_rel; | ||
| } | ||
|
|
||
| // Also resolve from the workspace root when running from within this repo | ||
| // (for example from `xtask/`), but do not leak into unrelated temp dirs. | ||
| let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR")) | ||
| .parent() | ||
| .unwrap_or(Path::new(env!("CARGO_MANIFEST_DIR"))); | ||
| if env::current_dir() | ||
| .ok() | ||
| .is_some_and(|cwd| cwd.starts_with(workspace_root)) | ||
| { | ||
| let workspace_rel = workspace_root.join(rel); | ||
| if workspace_rel.exists() { | ||
| return workspace_rel; | ||
| } | ||
| } | ||
|
|
||
| cwd_rel | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
workspace_path returns inconsistent path types (relative vs. absolute) across branches.
When cwd_rel.exists() is true the function returns a relative PathBuf; when the workspace-root branch fires it returns an absolute one; the fallback is relative again. This is harmless today because all callers re-check .exists() and pass the result to fs::read_dir, which resolves both correctly against the current CWD. However, if a future caller stores, serialises, or displays the returned path, the caller will silently receive different path forms depending on the environment. A straightforward fix is to always canonicalise to an absolute path:
♻️ Proposed refactor — always return absolute path
fn workspace_path(rel: &str) -> PathBuf {
let cwd_rel = PathBuf::from(rel);
+ // Resolve the CWD-relative path to an absolute path once.
+ let abs_cwd_rel = env::current_dir()
+ .map(|cwd| cwd.join(rel))
+ .unwrap_or_else(|_| cwd_rel.clone());
- if cwd_rel.exists() {
- return cwd_rel;
+ if abs_cwd_rel.exists() {
+ return abs_cwd_rel;
}
let workspace_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap_or(Path::new(env!("CARGO_MANIFEST_DIR")));
if env::current_dir()
.ok()
.is_some_and(|cwd| cwd.starts_with(workspace_root))
{
let workspace_rel = workspace_root.join(rel);
if workspace_rel.exists() {
return workspace_rel;
}
}
- cwd_rel
+ abs_cwd_rel
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@xtask/src/main.rs` around lines 872 - 894, workspace_path currently returns
sometimes-relative PathBufs (cwd_rel) and sometimes-absolute ones
(workspace_rel); change it to always produce an absolute/canonical PathBuf:
after selecting the candidate (cwd_rel or workspace_root.join(rel) as currently
done), call std::fs::canonicalize (or Path::canonicalize) on the chosen
candidate and return that result, falling back to the chosen candidate if
canonicalize fails; update logic in workspace_path that uses cwd_rel,
workspace_rel and workspace_root and ensure env::current_dir checks remain but
final return is the canonicalized absolute PathBuf for all branches.
|
|
||
| #[test] | ||
| fn list_fuzz_targets_returns_sorted_rs_stems() { | ||
| let _cwd_lock = CWD_LOCK.lock().unwrap(); |
There was a problem hiding this comment.
Mutex poisoning on test panic will cascade and mask the root failure.
If any test panics while holding CWD_LOCK (e.g., an assert_eq! mismatch or an .expect() inside the tested function), the Mutex is poisoned. Subsequent test threads that call .unwrap() on CWD_LOCK.lock() will also panic, turning one failure into many and obscuring the actual root cause. CwdGuard's Drop correctly restores the CWD, so filesystem state is clean; only the lock is poisoned.
🛡️ Proposed fix — recover from poisoned lock
- let _cwd_lock = CWD_LOCK.lock().unwrap();
+ let _cwd_lock = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());Apply this at all three lock sites (lines 1099, 1115, 1124).
Also applies to: 1115-1115, 1124-1124
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@xtask/src/main.rs` at line 1099, Replace the direct unwrap on the CWD mutex
to recover from poisoning: wherever you call CWD_LOCK.lock().unwrap() (notably
around creation of the CwdGuard), change it to
CWD_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) so a
previously-poisoned mutex does not cause cascading panics; apply this change at
all three lock sites that create the CwdGuard (references: CWD_LOCK and
CwdGuard).
| #[test] | ||
| fn x509_change_expands_to_tonic_adapter() { | ||
| let paths = vec!["crates/uselesskey-x509/src/lib.rs".to_string()]; | ||
| let plan = build_plan(&paths); | ||
| let impacted = &plan.impacted_crates; | ||
| assert!(impacted.contains("uselesskey-x509")); | ||
| assert!(impacted.contains("uselesskey-rustls")); | ||
| assert!(impacted.contains("uselesskey-tonic")); | ||
| } |
There was a problem hiding this comment.
x509_change_expands_to_tonic_adapter is missing an assertion for "uselesskey".
dependents("uselesskey-x509") returns &["uselesskey", "uselesskey-rustls", "uselesskey-tonic"], so "uselesskey" will always be present in the impacted set when x509 changes. The test omits this assertion, leaving a gap in coverage for a new test that should be comprehensive.
🛠️ Proposed fix
assert!(impacted.contains("uselesskey-x509"));
+ assert!(impacted.contains("uselesskey"));
assert!(impacted.contains("uselesskey-rustls"));
assert!(impacted.contains("uselesskey-tonic"));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@xtask/src/plan.rs` around lines 323 - 331, The test
x509_change_expands_to_tonic_adapter is missing an assertion for the root crate
"uselesskey"; update the test (in function x509_change_expands_to_tonic_adapter)
that builds the plan via build_plan(&paths) and checks plan.impacted_crates to
also assert that impacted_crates contains "uselesskey" (in addition to
"uselesskey-x509", "uselesskey-rustls", and "uselesskey-tonic") so the test
validates the full dependent set returned by dependents("uselesskey-x509").
- Format der.rs to pass CI fmt check - Use 2_000_000_000 for OAuth token exp (avoids 32-bit overflow) - Return winner Arc directly from cache_insert_if_absent to avoid TOCTOU race if clear_cache() interleaves between insert and re-get
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/uselesskey-core/src/factory.rs (1)
261-267: 🧹 Nitpick | 🔵 TrivialRedundant
.to_string()onlabelandvariant
ArtifactId::newacceptsimpl Into<String>, and&stralready implementsInto<String>. The.to_string()calls produce an unnecessary intermediate allocation.♻️ Proposed simplification
let id = ArtifactId::new( domain, - label.to_string(), + label, spec_bytes, - variant.to_string(), + variant, DerivationVersion::V1, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/src/factory.rs` around lines 261 - 267, The calls to .to_string() when constructing the ArtifactId are redundant because ArtifactId::new accepts impl Into<String> and &str already implements Into<String>; update the ArtifactId::new invocation (the id creation) to pass label and variant directly (keep domain and spec_bytes as before) and remove the intermediate .to_string() allocations so the signature remains ArtifactId::new(domain, label, spec_bytes, variant, DerivationVersion::V1).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-core/src/negative/der.rs`:
- Line 75: The current selection uses match bytes[0] % 3 which introduces a tiny
modulo bias; replace it with rejection sampling: find the first byte in the
provided bytes buffer with value < 255 and use (that_byte % 3) for bucket
selection, and only if no such byte exists (very unlikely) fall back to the
original bytes[0] % 3 to preserve behavior. Update the code around the match
bytes[0] % 3 site to locate the first acceptable byte (e.g.,
bytes.iter().find(|b| **b < 255)) and compute the bucket from that byte.
- Around line 71-73: The current corrupt_der_deterministic uses an unkeyed hash
via hash32(variant) which violates the crate's keyed-derivation rule; change
corrupt_der_deterministic(der: &[u8], variant: &str) to accept a master_seed
(e.g. master_seed: &[u8]) and replace the hash32 call with a BLAKE3 keyed
derivation: derive a seed using a keyed blake3 invocation (master_seed as key
and variant as context/artifact_id) to produce the digest bytes used for
deterministic corruption, or call the existing derive_seed helper if one exists
in derive.rs; ensure the function name corrupt_der_deterministic and its callers
are updated accordingly and document the reason if you intentionally want an
unkeyed exception.
In `@crates/uselesskey-token/src/token.rs`:
- Around line 49-57: TokenFixture::new currently hardcodes variant "good" so
negative/corrupt token variants aren't exposed; update the public API to accept
a variant and/or add an extension method on TokenFactoryExt (e.g.,
token_with_variant or generate_token) that forwards the variant into load_inner
instead of always using "good", and implement branching in the token generation
logic (generate_token) to return purposeful negative fixtures for "mismatch"
(truncated string, bad base64/padding, expired exp, mismatched keypair) while
keeping existing behavior for "good"; adjust TokenFixture::new (and any callers)
to use the new variant-capable factory method or accept a variant parameter so
downstream tests can request the "mismatch" variant.
---
Outside diff comments:
In `@crates/uselesskey-core/src/factory.rs`:
- Around line 261-267: The calls to .to_string() when constructing the
ArtifactId are redundant because ArtifactId::new accepts impl Into<String> and
&str already implements Into<String>; update the ArtifactId::new invocation (the
id creation) to pass label and variant directly (keep domain and spec_bytes as
before) and remove the intermediate .to_string() allocations so the signature
remains ArtifactId::new(domain, label, spec_bytes, variant,
DerivationVersion::V1).
---
Duplicate comments:
In `@crates/uselesskey-core/src/negative/der.rs`:
- Around line 100-107: The truncate function derived_truncate_len currently
yields zero with no variance for small inputs; modify derived_truncate_len so it
still returns 0 when len <= 1 but otherwise computes span = len - 1 and returns
1 + (digest[2] as usize % span) to ensure at least one byte is retained and
restore variance for len == 2; update the misleading comment to reflect that we
guarantee retaining at least one byte (not truncating by at least one), and
apply the identical fix to the test helper reference_truncate_len so both
implementations stay in sync.
In `@crates/uselesskey-token/src/token.rs`:
- Around line 130-141: The random_base62 function uses byte[0] % BASE62.len()
which introduces modulo bias; change to rejection sampling: compute let limit =
256 - (256 % BASE62.len()) and only accept bytes where byte[0] < limit (keep
calling rng.fill_bytes(&mut byte) until that condition), then set idx = (byte[0]
as usize) % BASE62.len(); keep the rest of random_base62 logic and references to
BASE62 and rng the same so output characters are selected uniformly.
- Around line 106-128: The token produced by generate_oauth_access_token is
JWT-shaped but the signature is just random bytes (signature and
signature_segment), which is incorrect; replace the random signature generation
with a real RS256 signature over the "{header}.{payload_segment}" bytes using a
private key so the "alg":"RS256" header matches the signing, i.e., compute the
signing input as format!("{header}.{payload_segment}"), sign it with an
RSA-SHA256 routine (using your project's crypto library), base64url-encode the
resulting signature into signature_segment, and use that in the final
format!("{header}.{payload_segment}.{signature_segment}") so header,
payload_segment and signature_segment (names from this function) remain the same
but the signature is cryptographically valid.
| pub fn corrupt_der_deterministic(der: &[u8], variant: &str) -> Vec<u8> { | ||
| let digest = hash32(variant.as_bytes()); | ||
| let bytes = digest.as_bytes(); |
There was a problem hiding this comment.
Plain BLAKE3 hash used instead of the required keyed BLAKE3 hash.
hash32 (in derive.rs) wraps blake3::hash(bytes), an unkeyed hash. The guideline for **/uselesskey-core/src/**/*.rs mandates keyed derivation: master_seed + artifact_id → derived_seed → RNG → artifact. Using a plain hash means the corruption strategy is wholly determined by the variant string with no master-seed isolation or domain separation, which is inconsistent with how other artifacts in this crate are derived.
If keyed derivation is intentionally skipped for corruption helpers (on the grounds that no primary artifact is being generated), that exception should be documented.
Based on learnings: "Use deterministic derivation via BLAKE3 keyed hash: master_seed + artifact_id -> derived_seed -> RNG -> artifact" (applies to **/uselesskey-core/src/**/*.rs).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core/src/negative/der.rs` around lines 71 - 73, The current
corrupt_der_deterministic uses an unkeyed hash via hash32(variant) which
violates the crate's keyed-derivation rule; change
corrupt_der_deterministic(der: &[u8], variant: &str) to accept a master_seed
(e.g. master_seed: &[u8]) and replace the hash32 call with a BLAKE3 keyed
derivation: derive a seed using a keyed blake3 invocation (master_seed as key
and variant as context/artifact_id) to produce the digest bytes used for
deterministic corruption, or call the existing derive_seed helper if one exists
in derive.rs; ensure the function name corrupt_der_deterministic and its callers
are updated accordingly and document the reason if you intentionally want an
unkeyed exception.
| let digest = hash32(variant.as_bytes()); | ||
| let bytes = digest.as_bytes(); | ||
|
|
||
| match bytes[0] % 3 { |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Slight modulo bias in corruption-strategy bucket selection.
bytes[0] % 3 over a uniform byte (256 values, 256 = 3 × 85 + 1) gives bucket 0 a 86/256 probability vs 85/256 for buckets 1 and 2. Negligible for test fixtures, but if strict uniformity matters, rejection-sampling or restricting to the first 255 values (multiple of 3) removes the bias.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core/src/negative/der.rs` at line 75, The current selection
uses match bytes[0] % 3 which introduces a tiny modulo bias; replace it with
rejection sampling: find the first byte in the provided bytes buffer with value
< 255 and use (that_byte % 3) for bucket selection, and only if no such byte
exists (very unlikely) fall back to the original bytes[0] % 3 to preserve
behavior. Update the code around the match bytes[0] % 3 site to locate the first
acceptable byte (e.g., bytes.iter().find(|b| **b < 255)) and compute the bucket
from that byte.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-core/src/negative/der.rs`:
- Around line 130-136: The reference implementation reference_truncate_len
currently duplicates the production formula, making
derived_truncate_len_matches_reference_formula a circular test; change
reference_truncate_len to express the intended semantics (e.g., include the
missing +1 offset or other documented intent) instead of mirroring
derived_truncate_len, then update derived_truncate_len to implement that
intended formula so the test actually verifies correctness; ensure you also
check and align any behavior for len <= 1 in reference_truncate_len and
derived_truncate_len and keep the test
derived_truncate_len_matches_reference_formula verifying equality between the
corrected reference and derived implementations.
---
Duplicate comments:
In `@crates/uselesskey-core/src/negative/der.rs`:
- Around line 71-73: The deterministic corruption currently calls the unkeyed
hash32(variant.as_bytes()), which violates the keyed-derivation contract; change
corrupt_der_deterministic to derive its digest with a BLAKE3 keyed hash that
includes the master seed and domain info instead of the bare hash32: either
accept a master_seed parameter (e.g., &master_seed: &[u8]) or call the crate's
centralized keyed-derive helper, then compute a keyed BLAKE3 output from
(master_seed || b"corrupt_der" || variant) (or use blake3::keyed_hash with a
32-byte key derived from master_seed) and replace the current digest =
hash32(...) / digest.as_bytes() usage so the corruption is deterministically
tied to the master seed and domain-separated by "corrupt_der" and variant.
- Around line 100-107: derived_truncate_len currently computes digest[2] as
usize % (len - 1) which can be 0 (contradicting the "Always truncate by at least
one byte" comment) and makes truncate_der receive len = 0; change the
computation so the result is in 1..(len-1) by adding 1 to the modulus result
(i.e., compute (digest[2] as usize % (len - 1)) + 1) so that
derived_truncate_len returns at least 1 for len > 1; update the function
derived_truncate_len accordingly and ensure truncate_der callers still expect a
positive truncate length.
- Line 75: The match expression using "bytes[0] % 3" introduces modulo bias;
change it to unbiased selection via rejection sampling: draw a byte from the
same source (e.g. from the bytes buffer) and if the value is >= 255 reject and
consume another byte until value < 255, then use (value % 3) for the match.
Update the code around the "match bytes[0] % 3" site in der.rs to perform this
loop and ensure you advance/consume bytes consistently when rejecting so you
don't reuse the same byte.
| fn reference_truncate_len(len: usize, digest: &[u8; 32]) -> usize { | ||
| if len <= 1 { | ||
| 0 | ||
| } else { | ||
| digest[2] as usize % (len - 1) | ||
| } | ||
| } |
There was a problem hiding this comment.
reference_truncate_len mirrors the production formula verbatim, making the consistency test circular.
fn reference_truncate_len(len: usize, digest: &[u8; 32]) -> usize {
if len <= 1 {
0
} else {
digest[2] as usize % (len - 1) // identical to derived_truncate_len
}
}derived_truncate_len_matches_reference_formula (line 235) compares derived_truncate_len against this function, but because both encode the exact same formula, the test always passes — including when the formula itself is wrong (e.g., the missing +1 offset flagged by the existing review comment). The reference function should encode the intended semantics, not just replicate the production code.
🛠️ Suggested correction to the reference
fn reference_truncate_len(len: usize, digest: &[u8; 32]) -> usize {
if len <= 1 {
0
} else {
- digest[2] as usize % (len - 1)
+ // Keep at least one byte: result is in 1..=len-1
+ 1 + digest[2] as usize % (len - 1)
}
}Then update derived_truncate_len accordingly so the test becomes a genuine correctness check.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core/src/negative/der.rs` around lines 130 - 136, The
reference implementation reference_truncate_len currently duplicates the
production formula, making derived_truncate_len_matches_reference_formula a
circular test; change reference_truncate_len to express the intended semantics
(e.g., include the missing +1 offset or other documented intent) instead of
mirroring derived_truncate_len, then update derived_truncate_len to implement
that intended formula so the test actually verifies correctness; ensure you also
check and align any behavior for len <= 1 in reference_truncate_len and
derived_truncate_len and keep the test
derived_truncate_len_matches_reference_formula verifying equality between the
corrected reference and derived implementations.
feat: add deterministic token generation with variants chore: refine negative fixtures example and adjust test utility usage
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/uselesskey-core/src/negative/pem.rs (1)
76-84: 🧹 Nitpick | 🔵 TrivialMinor:
Truncate { bytes }field name is misleading sincecorrupt_pemoperates onchars, not bytes.Line 81 does
pem.chars().take(bytes).collect(), sobytesis actually a character count. For ASCII PEM this is equivalent, but the field name could confuse callers with multi-byte content. This is pre-existing (the enum is not new), just noting it for awareness.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-core/src/negative/pem.rs` around lines 76 - 84, The Truncate variant's field is named `bytes` but `corrupt_pem` uses `pem.chars().take(bytes)` (a character count), which is misleading; rename the field to `chars` in the `CorruptPem::Truncate` variant and update all usages (including the match arm in `corrupt_pem` and any callers) to use the new `chars` identifier so the name accurately reflects it's a character count rather than bytes.crates/uselesskey-bdd/tests/bdd.rs (1)
3172-3182:⚠️ Potential issue | 🟡 Minor
gen_x509_chain_againdoes not updateworld.x509_chain_spec, unlikegen_x509_chain_second.After this step,
world.x509_chainholds the regenerated chain butworld.x509_chain_specstill points to the originalgen_x509_chainspec. If a step likeadd_san_to_chainis later called, it reads fromworld.x509_chain.spec()(correct), but any future step that readsworld.x509_chain_specdirectly would see stale data. Consider updating for consistency.♻️ Proposed fix
fn gen_x509_chain_again(world: &mut UselessWorld, domain: String, label: String) { let fx = world.factory.as_ref().expect("factory not set"); let spec = ChainSpec::new(&domain); - let chain = fx.x509_chain(&label, spec); + let chain = fx.x509_chain(&label, spec.clone()); world.x509_chain_leaf_der_2 = Some(chain.leaf_cert_der().to_vec()); world.x509_chain_root_der_2 = Some(chain.root_cert_der().to_vec()); world.x509_chain_leaf_pem_2 = Some(chain.leaf_cert_pem().to_string()); world.x509_chain_intermediate_pem_2 = Some(chain.intermediate_cert_pem().to_string()); world.x509_chain_root_pem_2 = Some(chain.root_cert_pem().to_string()); world.x509_chain = Some(chain); + world.x509_chain_spec = Some(spec); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 3172 - 3182, gen_x509_chain_again creates a new ChainSpec and x509 chain but doesn't update world.x509_chain_spec, leaving it stale; mirror gen_x509_chain_second by assigning the new spec into world.x509_chain_spec (e.g., set world.x509_chain_spec = Some(spec.clone() or spec.to_owned() / or use chain.spec()) after creating spec/chain) so both world.x509_chain and world.x509_chain_spec reflect the regenerated chain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-bdd/tests/bdd.rs`:
- Around line 1591-1604: gen_x509_chain_second currently overwrites
world.x509_chain so subsequent steps like get_unknown_ca_chain and
chain_root_differs_from_original compare the second chain as the "original"; to
fix, add a separate field (e.g., x509_chain_original: Option<X509Chain>) and
ensure gen_x509_chain stores the first generated chain into x509_chain_original
while gen_x509_chain_second only sets the second-specific fields (leaf_der_2,
root_der_2, etc.) and world.x509_chain if needed; then update
chain_root_differs_from_original to compare the unknown-CA variant against
world.x509_chain_original (or rename the step to
chain_root_differs_from_unknown_ca_root if you prefer the renaming approach).
- Around line 2254-2257: The step function build_jwks_all_three currently
delegates to build_jwks_all which may include an HMAC key when world.hmac is
set, producing 4 keys and breaking expectations; change build_jwks_all_three to
either (a) call a new helper that builds exactly three public keys (e.g.,
build_jwks_three_public) or (b) invoke build_jwks_all with an explicit
flag/filter to exclude the HMAC slot (check world.hmac) so the resulting JWKS
contains only the three public keys; update the function name or test step text
if you prefer the more general behavior so callers are not surprised.
In `@crates/uselesskey-token/src/token.rs`:
- Around line 74-83: TokenFixture currently stores a Factory only to support the
private, test-only load_variant method; since Factory is cheap (wraps
Arc<Inner>), either remove the field and pass Factory into load_variant when
needed, or keep it but add a short clarifying comment above the factory field
and retain #[allow(dead_code)] on load_variant. Locate the TokenFixture struct
and its new() constructor and either (A) remove the factory field and update
load_variant to take a Factory parameter, or (B) keep the factory field and add
a comment like "Stored for test-only load_variant; Factory is cheap
(Arc<Inner>)" to document the rationale (references: TokenFixture, factory, new,
load_variant, Factory, Inner).
---
Outside diff comments:
In `@crates/uselesskey-bdd/tests/bdd.rs`:
- Around line 3172-3182: gen_x509_chain_again creates a new ChainSpec and x509
chain but doesn't update world.x509_chain_spec, leaving it stale; mirror
gen_x509_chain_second by assigning the new spec into world.x509_chain_spec
(e.g., set world.x509_chain_spec = Some(spec.clone() or spec.to_owned() / or use
chain.spec()) after creating spec/chain) so both world.x509_chain and
world.x509_chain_spec reflect the regenerated chain.
In `@crates/uselesskey-core/src/negative/pem.rs`:
- Around line 76-84: The Truncate variant's field is named `bytes` but
`corrupt_pem` uses `pem.chars().take(bytes)` (a character count), which is
misleading; rename the field to `chars` in the `CorruptPem::Truncate` variant
and update all usages (including the match arm in `corrupt_pem` and any callers)
to use the new `chars` identifier so the name accurately reflects it's a
character count rather than bytes.
---
Duplicate comments:
In `@crates/uselesskey-bdd/tests/bdd.rs`:
- Around line 3106-3132: The function gen_same_keys_reverse_again uses hardcoded
labels ("isolation-rsa", "isolation-ecdsa", "isolation-ed25519") when
regenerating keys which can diverge from the original labels captured in
world.rsa_keys/word.ecdsa/world.ed25519; update the regeneration to reuse the
original labels from the snapshot (e.g., derive RSA labels from world.rsa_keys
entries, ECDSA label from world.ecdsa's label, Ed25519 label from
world.ed25519's label) when calling fx.rsa/fx.ecdsa/fx.ed25519 so the newly
created keys match the originals, then replace clearing/pushing logic for
world.rsa_keys/world.rsa/world.ecdsa/world.ed25519 to preserve order consistent
with the snapshot used for comparison in regenerated_keys_identical.
- Around line 1882-1907: Both step definitions assume RSA keys and will panic or
mis-parse non-RSA leaf keys; instead decode the PKCS#8 blob generically, inspect
the algorithm OID, and branch to the correct key parser (RSA, EC, Ed25519) so
the tests work for other ChainSpec key types. Concretely, in
leaf_private_key_is_valid_pkcs8 use a generic PKCS#8 decode (e.g.
pkcs8::PrivateKeyInfo::from_der or equivalent) and validate the structure rather
than calling rsa::RsaPrivateKey::from_pkcs8_der unconditionally; in
leaf_private_key_matches_leaf_cert parse the leaf private PKCS#8 into the
correct key type by matching the alg OID, extract the certificate SPKI bytes
from the x509_parser SubjectPublicKeyInfo API (verify and use subject_public_key
/ the correct .raw field for x509-parser 0.18.1), parse the cert public key with
the corresponding public-key parser for the algorithm, and compare public
components only within each algorithm-specific branch (e.g. modulus/exponent for
RSA, public point for EC, public key bytes for Ed25519).
In `@crates/uselesskey-core/src/negative/der.rs`:
- Around line 64-95: The doc comment correctly documents that
corrupt_der_deterministic intentionally derives corruption only from the variant
string (not the factory seed); no code change is needed—leave the function
corrupt_der_deterministic and its existing comment as-is to preserve this
documented exception to keyed derivation.
- Around line 104-111: The DER-derived_truncate_len currently uses digest[2] %
span and can return 0 and only 8 bits of entropy; change derived_truncate_len to
mirror PEM by (a) ensuring the result is at least 1 when len > 1 (e.g. return 1
+ (... % span)) and (b) use two bytes for entropy (e.g.
u16::from_be_bytes([digest[1], digest[2]]) as usize) so truncation length ranges
across 16 bits; keep the early return for len <= 1 returning 0 and compute span
= len - 1 as before.
In `@crates/uselesskey-core/src/negative/pem.rs`:
- Around line 97-111: In corrupt_pem_deterministic rename the first `bytes`
(from hash32(...).as_bytes()) to `digest_bytes` and rename the second `bytes`
(result of derived_truncate_len) to `truncate_len` to avoid shadowing; update
the match arm that uses `bytes[0]` to `digest_bytes[0]` and the Truncate call to
use `truncate_len`, leaving uses of hash32, derived_truncate_len, CorruptPem,
and corrupt_pem unchanged.
In `@crates/uselesskey-token/src/token.rs`:
- Around line 137-159: The function generate_oauth_access_token produces a
JWT-shaped string but uses random bytes for the signature (the header claims
"RS256" while signature is non-cryptographic), so add a concise doc comment on
generate_oauth_access_token stating it returns a structurally valid JWT-like
fixture token that is not cryptographically signed or verifiable (e.g., header
claims RS256 but signature is random bytes) so future readers understand it is
for testing only; reference generate_oauth_access_token, payload_segment and
signature_segment to make the intent explicit.
- Around line 108-115: load_inner passes variant into factory.get_or_init
(affecting the RNG seed) but never forwards it to generate_token, so variant
currently only alters deterministic seeding instead of producing a
structurally-corrupt token; either (A) change the generate_token API and call in
load_inner to accept &str variant and make token generation reflect variant, or
(B) if the intent is that variant only affects caching/seeding, add a clear doc
comment on load_inner stating that variant is used only for cache-keying/RNG
seeding and does not change the token format (and keep DOMAIN_TOKEN_FIXTURE /
factory.get_or_init usage as-is); update any related tests to assert the chosen
behavior.
- Around line 39-72: generate_token is not taking the variant so all variants
produce structurally identical tokens; update generate_token to accept a variant
parameter (e.g., &str or impl AsRef<str>) and propagate the variant from
TokenFactoryExt::token_with_variant (which builds inner via load_inner) and
TokenFixture constructors (TokenFixture::new and the TokenFixture literal) so
load_inner and any seeding/structure logic use the variant to produce differing
token structure for "mismatch" and other variants; adjust all call sites
(including where TokenFixture::new is used) to pass the variant through to
generate_token.
- Around line 161-172: The current random_base62 uses byte[0] % BASE62.len(),
which introduces modulo bias because 256 is not a multiple of 62; fix by using
rejection sampling in random_base62: when you read a byte from rng in
random_base62, only accept it if byte[0] < 62 * (256 / 62) (i.e. < 248) and
otherwise discard and draw another byte, then use the accepted byte %
BASE62.len() to index BASE62; update the loop in random_base62 to perform this
check so BASE62 indexing is unbiased.
In `@crates/uselesskey/examples/negative_fixtures.rs`:
- Around line 9-18: The example is only cfg-gated by feature "x509" but calls
rsa APIs (RsaSpec::rs256 and fx.rsa("primary", ...)), so update the cfgs and the
example run comment to require both features; change the module-level
#[cfg(feature = "x509")] guards to #[cfg(all(feature = "x509", feature =
"rsa"))] for imports/usages that touch RsaSpec and fx.rsa, and update the top
comment run line to `--features "rsa,x509"` so compiling with only x509 no
longer fails.
In `@tests/key_rotation.rs`:
- Line 25: Replace the hardcoded expiration timestamp exp: 2_000_000_000 in the
test claims (in tests/key_rotation.rs) with a far-future safe value such as exp:
4_000_000_000 so the test JWTs won't expire in 2033 while still staying within
u32 limits; update the claim/Token construction site where exp is set to use
4_000_000_000 (or a similarly large constant) and run tests to confirm decode
assertions remain is_ok().
| fn gen_x509_chain_second(world: &mut UselessWorld, domain: String, label: String) { | ||
| let fx = world.factory.as_ref().expect("factory not set"); | ||
| let spec = ChainSpec::new(&domain); | ||
| let chain = fx.x509_chain(&label, spec.clone()); | ||
|
|
||
| world.label = Some(label); | ||
| world.x509_chain_leaf_der_2 = Some(chain.leaf_cert_der().to_vec()); | ||
| world.x509_chain_root_der_2 = Some(chain.root_cert_der().to_vec()); | ||
| world.x509_chain_leaf_pem_2 = Some(chain.leaf_cert_pem().to_string()); | ||
| world.x509_chain_intermediate_pem_2 = Some(chain.intermediate_cert_pem().to_string()); | ||
| world.x509_chain_root_pem_2 = Some(chain.root_cert_pem().to_string()); | ||
| world.x509_chain = Some(chain); | ||
| world.x509_chain_spec = Some(spec); | ||
| } |
There was a problem hiding this comment.
world.x509_chain is overwritten, making chain_root_differs_from_original semantically misleading.
After gen_x509_chain_second, world.x509_chain holds the second chain. Any scenario that subsequently calls get_unknown_ca_chain and chain_root_differs_from_original will compare the second chain's root (labelled "original" inside that step) against the unknown-CA variant — not the first chain's root. The test logic remains correct as long as both steps operate on the same chain, but the naming is misleading.
Consider one of:
- Storing a separate
x509_chain_original: Option<X509Chain>captured only bygen_x509_chain, or - Renaming
chain_root_differs_from_originaltochain_root_differs_from_unknown_ca_root.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 1591 - 1604,
gen_x509_chain_second currently overwrites world.x509_chain so subsequent steps
like get_unknown_ca_chain and chain_root_differs_from_original compare the
second chain as the "original"; to fix, add a separate field (e.g.,
x509_chain_original: Option<X509Chain>) and ensure gen_x509_chain stores the
first generated chain into x509_chain_original while gen_x509_chain_second only
sets the second-specific fields (leaf_der_2, root_der_2, etc.) and
world.x509_chain if needed; then update chain_root_differs_from_original to
compare the unknown-CA variant against world.x509_chain_original (or rename the
step to chain_root_differs_from_unknown_ca_root if you prefer the renaming
approach).
| #[when("I build a JWKS containing all three keys")] | ||
| fn build_jwks_all_three(world: &mut UselessWorld) { | ||
| build_jwks_all(world); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
build_jwks_all_three delegates to build_jwks_all, which can emit 4 keys.
build_jwks_all includes HMAC if world.hmac is set. If any scenario sets up HMAC alongside three public keys and then uses this step, the produced JWKS will contain 4 keys, silently breaking assertions like "the JWKS should contain 3 keys". Consider guarding explicitly against the HMAC slot, or renaming to a more general alias.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd/tests/bdd.rs` around lines 2254 - 2257, The step
function build_jwks_all_three currently delegates to build_jwks_all which may
include an HMAC key when world.hmac is set, producing 4 keys and breaking
expectations; change build_jwks_all_three to either (a) call a new helper that
builds exactly three public keys (e.g., build_jwks_three_public) or (b) invoke
build_jwks_all with an explicit flag/filter to exclude the HMAC slot (check
world.hmac) so the resulting JWKS contains only the three public keys; update
the function name or test step text if you prefer the more general behavior so
callers are not surprised.
| impl TokenFixture { | ||
| fn new(factory: Factory, label: &str, spec: TokenSpec) -> Self { | ||
| let inner = load_inner(&factory, label, spec, "good"); | ||
| Self { | ||
| factory, | ||
| label: label.to_string(), | ||
| spec, | ||
| inner, | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if Factory uses Arc internally
ast-grep --pattern $'pub struct Factory {
$$$
}'Repository: EffortlessMetrics/uselesskey
Length of output: 240
🏁 Script executed:
# Also read the token.rs file to see the full context
cat -n crates/uselesskey-token/src/token.rs | head -100Repository: EffortlessMetrics/uselesskey
Length of output: 3453
🏁 Script executed:
# Find load_variant definition and usage
rg "load_variant" crates/uselesskey-token/src/token.rs -B 3 -A 10Repository: EffortlessMetrics/uselesskey
Length of output: 1023
factory field in TokenFixture is exclusively needed for test-only load_variant method.
Since Factory uses Arc<Inner> internally, storing and cloning it is cheap. The #[allow(dead_code)] on load_variant is appropriate since it's private and only used by tests. If the design changes (e.g., load_variant removed or made public), consider documenting the rationale for storing factory.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-token/src/token.rs` around lines 74 - 83, TokenFixture
currently stores a Factory only to support the private, test-only load_variant
method; since Factory is cheap (wraps Arc<Inner>), either remove the field and
pass Factory into load_variant when needed, or keep it but add a short
clarifying comment above the factory field and retain #[allow(dead_code)] on
load_variant. Locate the TokenFixture struct and its new() constructor and
either (A) remove the factory field and update load_variant to take a Factory
parameter, or (B) keep the factory field and add a comment like "Stored for
test-only load_variant; Factory is cheap (Arc<Inner>)" to document the rationale
(references: TokenFixture, factory, new, load_variant, Factory, Inner).
Summary
Cumulative changes for the 0.3.0 release: two new key-type crates, a new adapter crate, deterministic corruption helpers, and a positioning refresh across README/Cargo metadata/GitHub topics.
New crates
uselesskey-pgp— OpenPGP key fixtures viaPgpFactoryExt. Supports RSA-2048, RSA-3072, and Ed25519 key types with armored and binary output, corruption helpers, and mismatch variants. Uses thepgpcrate under the hood.uselesskey-token— Token-shaped fixtures viaTokenFactoryExt. Generates API-key (uk_test_<base62>), opaque bearer (base64url), and OAuth access token (JWT-shapedheader.payload.signature) strings. Includesauthorization_header()convenience method.uselesskey-tonic— Adapter crate fortonicgRPC transport TLS. Extension traits (TonicIdentityExt,TonicServerTlsExt,TonicClientTlsExt,TonicMtlsExt) convert X.509 fixtures intotonic::transporttypes for server/client/mTLS test configurations.Deterministic corruption helpers (
uselesskey-core)corrupt_pem_deterministic(pem, variant)— Maps a variant string to one of 5 PEM corruption strategies (bad header, bad footer, bad base64, extra blank line, truncation) via BLAKE3 hash of the variant. Same input + same variant = same output.corrupt_der_deterministic(der, variant)— Maps a variant string to one of 3 DER corruption strategies (truncation, byte flip, flip+truncate). Enables stable"corrupt:*"negative fixtures without enum boilerplate.uselesskey_core::negative.Positioning refresh
"Deterministic and random cryptographic fixtures for tests."→"Deterministic cryptographic test fixtures for Rust."— leads with the differentiator, drops "and random" which dilutes uniqueness."Runtime cryptographic test fixtures for Rust with deterministic derivation, caching, and negative variants."→"Deterministic cryptographic key and certificate fixtures for Rust tests. Seed-stable, cached, scanner-safe."— more evocative, under 128 chars."Deterministic cryptographic key and certificate fixtures for Rust tests. Never commit secrets again."security,pki,jwt,tls,keys) with specific, discoverable ones (deterministic,rsa,ecdsa,ed25519,jwk,pem,certificate,secret-scanning,dev-tools,fixtures).Other changes in this branch
Cargo.lockget_or_initOnceLockpattern across crate test suites, improved rustls provider installation, NASM compatibility checks, expiration time normalization to2_000_000_000pgpandtokenfeature flags,all-keysbundle now includespgpTest plan
cargo xtask cipassescargo xtask publish-preflightmetadata validation passes (packaging step for unpublished deps is expected to fail)cargo xtask feature-matrix— verify new feature flags (pgp,token) compose correctlycargo xtask bdd— BDD scenarios cover new key types.github/settings.yml)