Skip to content

Release 0.3.0: PGP, tokens, tonic adapter, corruption fixtures, positioning refresh#4

Merged
EffortlessSteven merged 15 commits into
mainfrom
chore/v0.3.0-release
Feb 18, 2026
Merged

Release 0.3.0: PGP, tokens, tonic adapter, corruption fixtures, positioning refresh#4
EffortlessSteven merged 15 commits into
mainfrom
chore/v0.3.0-release

Conversation

@EffortlessSteven

@EffortlessSteven EffortlessSteven commented Feb 18, 2026

Copy link
Copy Markdown
Member

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 via PgpFactoryExt. Supports RSA-2048, RSA-3072, and Ed25519 key types with armored and binary output, corruption helpers, and mismatch variants. Uses the pgp crate under the hood.
  • uselesskey-token — Token-shaped fixtures via TokenFactoryExt. Generates API-key (uk_test_<base62>), opaque bearer (base64url), and OAuth access token (JWT-shaped header.payload.signature) strings. Includes authorization_header() convenience method.
  • uselesskey-tonic — Adapter crate for tonic gRPC transport TLS. Extension traits (TonicIdentityExt, TonicServerTlsExt, TonicClientTlsExt, TonicMtlsExt) convert X.509 fixtures into tonic::transport types 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.
  • Both are re-exported from uselesskey_core::negative.

Positioning refresh

  • README tagline (line 3): "Deterministic and random cryptographic fixtures for tests.""Deterministic cryptographic test fixtures for Rust." — leads with the differentiator, drops "and random" which dilutes uniqueness.
  • Cargo.toml description (facade crate): "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.
  • GitHub settings.yml description: updated to "Deterministic cryptographic key and certificate fixtures for Rust tests. Never commit secrets again."
  • GitHub topics: expanded from 10 → 15, replacing generic terms (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

  • Version bump to 0.3.0 across all workspace crates and Cargo.lock
  • Re-entrancy safety documentation and test for get_or_init
  • Test infrastructure: consistent factory initialization via OnceLock pattern across crate test suites, improved rustls provider installation, NASM compatibility checks, expiration time normalization to 2_000_000_000
  • Crate metadata refresh: updated descriptions and keywords across all sub-crates for consistency
  • Facade crate: added pgp and token feature flags, all-keys bundle now includes pgp

Test plan

  • cargo xtask ci passes
  • cargo xtask publish-preflight metadata validation passes (packaging step for unpublished deps is expected to fail)
  • cargo xtask feature-matrix — verify new feature flags (pgp, token) compose correctly
  • cargo xtask bdd — BDD scenarios cover new key types
  • GitHub repo description and topics update after merge (via .github/settings.yml)
  • Verify crate description renders correctly on crates.io after publish

…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.
Copilot AI review requested due to automatic review settings February 18, 2026 02:44
@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features
    • Token fixtures (API key, Bearer, OAuth access tokens)
    • OpenPGP keypair fixtures (RSA 2048/3072, Ed25519)
    • TLS/gRPC adapter for tonic (convert fixtures to tonic TLS types)
  • Enhancements
    • Deterministic PEM/DER corruption helpers for repeatable negative tests
    • Broader deterministic fixture APIs and expanded test/BDD coverage
    • no_std-compatible core with deterministic derivation and caching
  • Updates
    • Workspace crates and docs bumped to 0.3.0 with refreshed examples and README guidance

Walkthrough

This 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

Cohort / File(s) Summary
Core Infrastructure
crates/uselesskey-core/Cargo.toml, crates/uselesskey-core/src/lib.rs, crates/uselesskey-core/src/factory.rs, crates/uselesskey-core/src/id.rs, crates/uselesskey-core/src/error.rs, crates/uselesskey-core/src/derive.rs, crates/uselesskey-core/src/factory_tests.rs, crates/uselesskey-core/tests/*
0.3.0 bump; add #[cfg_attr(not(feature="std"), no_std)]; std-gated Io variant; cache abstraction (DashMap vs Mutex+BTreeMap), CacheValue alias, reentrant-safe get_or_init, deterministic-from-env gated to std, tests gated to std.
Deterministic Corruption Helpers
crates/uselesskey-core/src/negative/der.rs, crates/uselesskey-core/src/negative/pem.rs, crates/uselesskey-core/src/negative/mod.rs
Add corrupt_der_deterministic and corrupt_pem_deterministic (variant→stable transform), helper functions, and extensive stability tests.
New Token Fixtures Crate
crates/uselesskey-token/Cargo.toml, crates/uselesskey-token/src/lib.rs, crates/uselesskey-token/src/spec.rs, crates/uselesskey-token/src/token.rs, crates/uselesskey-token/README.md
New crate uselesskey-token: TokenSpec, TokenFixture, TokenFactoryExt with fx.token(label,spec); deterministic API-key, bearer, OAuth-style token generation and caching; tests and README.
New OpenPGP Fixtures Crate
crates/uselesskey-pgp/Cargo.toml, crates/uselesskey-pgp/src/lib.rs, crates/uselesskey-pgp/src/spec.rs, crates/uselesskey-pgp/src/keypair.rs, crates/uselesskey-pgp/README.md
New crate uselesskey-pgp: PgpSpec, PgpKeyPair, PgpFactoryExt; deterministic armored/binary PGP fixtures, corruption helpers, caching, and tests.
New tonic gRPC Integration Crate
crates/uselesskey-tonic/Cargo.toml, crates/uselesskey-tonic/src/lib.rs, crates/uselesskey-tonic/README.md
New adapter uselesskey-tonic: x509-gated traits to convert fixtures into tonic Identity, Certificate, ServerTlsConfig, ClientTlsConfig and mTLS builders; tests and docs.
Key-type Crates (RSA/ ECDSA/ Ed25519 / HMAC / JWK)
crates/uselesskey-rsa/*, crates/uselesskey-ecdsa/*, crates/uselesskey-ed25519/*, crates/uselesskey-hmac/*, crates/uselesskey-jwk/*
Bumped to 0.3.0; add deterministic corruption helpers and methods, JWK/alignment helpers, coordinate_len/jwk_alg helpers, expanded tests using deterministic factory helpers.
X.509 Fixtures & Chain Handling
crates/uselesskey-x509/*
0.3.0 bump; SANs deduplicated+sorted for stable_bytes, deterministic cert corruption methods added, stable_bytes version bumps and test updates.
Adapters & Providers
crates/uselesskey-jsonwebtoken/*, crates/uselesskey-rustls/*, crates/uselesskey-ring/*, crates/uselesskey-rustcrypto/*, crates/uselesskey-aws-lc-rs/*
Workspace-wide metadata and dependency bumps to 0.3.0; aws-lc-rs NASM gating broadened (allow non-windows without NASM); test examples normalized (exp timestamps) and deterministic test scaffolding added.
Facade / Main Crate & Examples
crates/uselesskey/Cargo.toml, crates/uselesskey/src/lib.rs, crates/uselesskey/examples/negative_fixtures.rs, crates/uselesskey/tests/*
Facade now exposes token and pgp features and prelude re-exports; examples gated by features; tests adopt deterministic test utilities (fx()).
BDD & Test Suites / Tooling
crates/uselesskey-bdd/*, tests/*, xtask/*
BDD world extended (second X.509 chain, many new step defs for JWKS/DER/PEM/tempfiles), shared cached X.509 fixtures added to tests/e2e, xtask publish order and paths updated, CI/test workflows adjusted.
Docs, Changelog & Repo Metadata
.github/settings.yml, CHANGELOG.md, README.md, CLAUDE.md, docs/*
Repository description/topics updated; changelog adds 0.3.0 Unreleased entry; README/docs/architecture/roadmap updated to reflect new crates, API signature changes, deterministic/no_std notes and release instructions.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 In burrows neat I tune the seed,

Tokens, keys, and certs I breed,
Deterministic hops that never roam,
Cached and stable, safe at home,
A tiny rabbit hums: release with glee!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main release focus: version 0.3.0 with three new crates (PGP, tokens, tonic), deterministic corruption fixtures, and positioning refresh.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing new crates, deterministic corruption helpers, positioning updates, and test infrastructure improvements that align with the file summaries.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/v0.3.0-release

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@EffortlessSteven EffortlessSteven changed the title Release 0.3.0: PGP, tokens, corruption fixtures, and positioning refresh Release 0.3.0: PGP, tokens, tonic adapter, corruption fixtures, positioning refresh Feb 18, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @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 uselesskey ecosystem by introducing new microcrates for OpenPGP and token fixtures, enhancing the library's utility for generating diverse cryptographic test data. A major focus was also placed on improving negative testing capabilities with new deterministic corruption helpers and ensuring the core library is compatible with no_std environments. These changes collectively broaden the scope of test scenarios that can be reliably and reproducibly covered, while also refining the project's overall positioning and discoverability.

Highlights

  • New Microcrates: Introduced uselesskey-pgp for OpenPGP key fixtures (RSA 2048/3072, Ed25519) and uselesskey-token for API key, bearer, and OAuth/JWT-shape token fixtures. A new uselesskey-tonic adapter crate was also added to convert X.509 fixtures into tonic::transport TLS types for gRPC testing.
  • Deterministic Corruption Helpers: Added new corrupt_pem_deterministic and corrupt_der_deterministic functions to uselesskey-core, along with convenience methods on key/cert fixtures, enabling stable negative fixture variants for testing error handling.
  • No-std Support for uselesskey-core: The core crate now supports no_std builds, with deterministic derivation, caching, and negative helpers compiling without default features. Random mode and environment seed helpers are gated behind the std feature.
  • Re-entrancy Safety for get_or_init: The get_or_init method in uselesskey-core was updated to ensure re-entrancy safety, allowing initialization closures to call back into get_or_init without deadlocking.
  • Positioning and Discoverability Refresh: Updated the main README.md tagline, Cargo.toml descriptions, and GitHub topics across the workspace to emphasize determinism as a core differentiator and improve crate discoverability.
  • Version Bump: All workspace crates have been bumped to version 0.3.0.
Changelog
  • .github/settings.yml
    • Updated repository description to "Deterministic cryptographic key and certificate fixtures for Rust tests. Never commit secrets again."
    • Updated repository topics to include test-fixtures, deterministic, rsa, ecdsa, ed25519, jwk, pem, certificate, secret-scanning, and dev-tools.
  • CHANGELOG.md
    • Added deterministic negative-fixture helpers in uselesskey-core for PEM and DER formats.
    • Added deterministic corruption convenience methods on key/cert fixtures (RSA/ECDSA/Ed25519, X.509).
    • Added new uselesskey-token microcrate with TokenFactoryExt and TokenSpec for API key, bearer, and OAuth access token fixtures.
    • Added new facade token feature in uselesskey.
    • Added new uselesskey-pgp microcrate with PgpFactoryExt and PgpSpec for armored and binary OpenPGP keyblock outputs.
    • Added new facade pgp feature in uselesskey.
    • Added new uselesskey-tonic adapter microcrate for converting uselesskey-x509 fixtures into tonic::transport TLS types.
    • uselesskey-core now supports no_std builds with a new std default feature gate.
  • CLAUDE.md
    • Updated description of negative fixtures to include deterministic corruption variants.
    • Added crates/uselesskey-token to the list of microcrates.
    • Updated testing instructions for cargo xtask test and cargo xtask bdd.
  • Cargo.toml
    • Added crates/uselesskey-token, crates/uselesskey-pgp, and crates/uselesskey-tonic to workspace members.
  • README.md
    • Updated tagline to "Deterministic cryptographic test fixtures for Rust."
    • Added OpenPGP and Token fixtures to the list of supported key types.
    • Added OpenPGP armored and binary keyblocks to output formats.
    • Updated uselesskey dev-dependency version to 0.3.
    • Added Token Fixtures example.
    • Added uselesskey-tonic to the adapter crates table.
    • Updated feature flag descriptions to include pgp and token.
  • crates/uselesskey-aws-lc-rs/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated dependency versions for uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, and uselesskey-core to 0.3.0.
    • Updated all feature to include native.
  • crates/uselesskey-aws-lc-rs/README.md
    • Updated description and feature table for clarity.
    • Updated example dev-dependencies version to 0.3 and features.
  • crates/uselesskey-aws-lc-rs/build.rs
    • Added cargo::rerun-if-changed and cargo::rerun-if-env-changed directives.
    • Refined NASM detection warnings.
  • crates/uselesskey-aws-lc-rs/src/lib.rs
    • Updated cfg_attr conditions for doc tests to include any(not(windows), has_nasm).
    • Updated cfg conditions for AwsLcRsRsaKeyPairExt, AwsLcRsEcdsaKeyPairExt, and AwsLcRsEd25519KeyPairExt to include any(not(windows), has_nasm).
    • Introduced OnceLock and fx() helper for test setup.
  • crates/uselesskey-aws-lc-rs/tests/aws_lc_rs_comprehensive.rs
    • Updated cfg conditions for various modules and use statements to include any(not(windows), has_nasm).
  • crates/uselesskey-bdd/Cargo.toml
    • Added uk-pgp feature.
  • crates/uselesskey-bdd/tests/bdd.rs
    • Added new fields to UselessWorld for X.509 chain PEMs and DERs, and new negative variants (x509_chain_unknown_ca).
    • Added ecdsa.clone() to gen_ecdsa_es384 to store multiple ECDSA keys.
    • Added gen_x509_chain_second function to generate a second certificate chain.
    • Added get_unknown_ca_chain function for X.509 negative variant.
    • Modified add_san_to_chain to use chain.label() if world.label is not set.
    • Added functions to write leaf certificate and private key PEMs to tempfiles.
    • Added new then steps for X.509 chain PEM identity, DER differences, common names, leaf/root certificate DER differences, private key validation, tempfile existence, and chain root differences.
    • Updated revoked_leaf_has_crl to check CRL content instead of extension presence.
    • Added build_jwks_all_three alias.
    • Added new then steps for JWK kty, crv, alg assertions, and RSA modulus length differences.
    • Added gen_same_keys_reverse_again to regenerate keys and snapshot PEMs.
    • Updated gen_x509_chain_again to store more PEM/DER outputs.
  • crates/uselesskey-core/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Added std feature as default, gating dashmap, rand, tempfile, thiserror/std, blake3/std, and rand_chacha/std.
    • Updated thiserror and blake3 dependencies to default-features = false.
    • Made dashmap, rand, and tempfile optional dependencies.
    • Added spin dependency for no_std mutex.
  • crates/uselesskey-core/README.md
    • Updated description and added 'What It Provides' and 'Features' sections.
    • Updated example to use Factory::deterministic_from_env and assert Mode::Deterministic.
  • crates/uselesskey-core/src/derive.rs
    • Added cfg attributes to eprintln! for no_std compatibility.
  • crates/uselesskey-core/src/error.rs
    • Added alloc::string::String import.
    • Added cfg attribute to Io error variant and its test to be std-only.
  • crates/uselesskey-core/src/factory.rs
    • Updated imports for no_std compatibility.
    • Introduced CacheValue and conditional Cache types (DashMap for std, Mutex<BTreeMap> for no_std).
    • Updated Factory::fmt to use cache_len helper.
    • Updated Factory::new to use new_cache helper.
    • Added no_std panic message for Mode::Random.
    • Added cfg(feature = "std") to Factory::deterministic_from_env.
    • Updated Factory::clear_cache to use cache_clear helper.
    • Updated Factory::get_or_init for re-entrancy safety by computing outside the cache lock and using cache_insert_if_absent.
    • Added new_cache, cache_len, cache_clear, cache_get, cache_insert_if_absent helper functions with cfg attributes for std/no_std.
    • Added cfg attributes to random_seed for std/no_std.
  • crates/uselesskey-core/src/factory_tests.rs
    • Added Seed import.
    • Added get_or_init_reentrant_does_not_deadlock test case.
  • crates/uselesskey-core/src/id.rs
    • Added alloc::string::String and core::fmt imports.
    • Updated parse_hex_32 to use alloc::format!.
    • Added PartialOrd and Ord traits to DerivationVersion and ArtifactId.
    • Updated cfg attribute for tests to include feature = "std".
  • crates/uselesskey-core/src/lib.rs
    • Added #![cfg_attr(not(feature = "std"), no_std)].
    • Added cfg(feature = "std") to sink module.
    • Added extern crate alloc;.
    • Updated cfg attribute for factory_tests to include feature = "std".
  • crates/uselesskey-core/src/negative/der.rs
    • Added alloc::vec::Vec and crate::derive::hash32 imports.
    • Added corrupt_der_deterministic function for stable DER corruption.
    • Added derived_offset and derived_truncate_len helper functions.
    • Updated cfg attribute for tests to include feature = "std".
    • Added tests for deterministic DER corruption stability, variation, empty input, bucket mapping, and derived offset/truncate length.
  • crates/uselesskey-core/src/negative/mod.rs
    • Updated documentation to mention deterministic corruption variants.
    • Exported corrupt_der_deterministic and corrupt_pem_deterministic.
  • crates/uselesskey-core/src/negative/pem.rs
    • Added alloc::string::{String, ToString}, alloc::vec::Vec, and crate::derive::hash32 imports.
    • Added corrupt_pem_deterministic function for stable PEM corruption.
    • Added derived_truncate_len helper function.
    • Updated inject_bad_base64_line and inject_blank_line to use alloc::format!.
    • Updated cfg attribute for tests to include feature = "std".
    • Added tests for deterministic PEM corruption stability, multiple shapes, short inputs, bucket mapping, and derived truncate length.
  • crates/uselesskey-core/tests/core_prop.rs
    • Added #![cfg(feature = "std")].
  • crates/uselesskey-core/tests/factory_env.rs
    • Added #![cfg(feature = "std")].
  • crates/uselesskey-core/tests/negative_prop.rs
    • Added #![cfg(feature = "std")].
  • crates/uselesskey-ecdsa/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated uselesskey-core and uselesskey-jwk dependency versions to 0.3.0.
  • crates/uselesskey-ecdsa/README.md
    • Updated description and feature table.
    • Updated example to use keypair variable and assert PEM/DER content.
  • crates/uselesskey-ecdsa/src/keypair.rs
    • Added corrupt_der_deterministic and corrupt_pem_deterministic imports.
    • Added private_key_pkcs8_pem_corrupt_deterministic and private_key_pkcs8_der_corrupt_deterministic methods.
    • Added assertions for EC point length based on spec.coordinate_len_bytes().
  • crates/uselesskey-ecdsa/src/spec.rs
    • Added coordinate_len_bytes method.
    • Added tests for coordinate_len_bytes.
  • crates/uselesskey-ecdsa/tests/jwk_private.rs
    • Added decode_b64url and expected_kid helper functions.
    • Added tests for kid_matches_public_key_hash_prefix, public_jwk_coordinates_match_public_key_material, and private_jwk_matches_private_scalar_and_public_coordinates.
  • crates/uselesskey-ecdsa/tests/keypair.rs
    • Added assertions for bad_pem content.
    • Added deterministic_corruption_helpers_are_stable test.
  • crates/uselesskey-ed25519/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated uselesskey-core and uselesskey-jwk dependency versions to 0.3.0.
  • crates/uselesskey-ed25519/README.md
    • Updated description and feature table.
    • Updated example to use keypair variable and assert PEM/DER content.
  • crates/uselesskey-ed25519/src/keypair.rs
    • Added corrupt_der_deterministic and corrupt_pem_deterministic imports.
    • Added private_key_pkcs8_pem_corrupt_deterministic and private_key_pkcs8_der_corrupt_deterministic methods.
  • crates/uselesskey-ed25519/tests/jwk_private.rs
    • Added decode_b64url and expected_kid helper functions.
    • Added tests for kid_matches_public_key_hash_prefix, public_jwk_x_matches_spki_public_key_bytes, and private_jwk_d_matches_pkcs8_secret_bytes.
  • crates/uselesskey-ed25519/tests/keypair.rs
    • Added assertions for bad_pem content.
    • Added deterministic_corruption_helpers_are_stable test.
  • crates/uselesskey-hmac/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated uselesskey-core and uselesskey-jwk dependency versions to 0.3.0.
    • Removed uselesskey-core from dev-dependencies.
  • crates/uselesskey-hmac/README.md
    • Updated description and added 'Features' and 'Example' sections.
  • crates/uselesskey-hmac/tests/hmac_unit.rs
    • Updated test_different_specs_different_secrets to assert secret_bytes() directly.
    • Updated test_debug_does_not_leak_secret_material to use debug_output variable.
  • crates/uselesskey-jsonwebtoken/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated dependency versions for uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, and uselesskey-hmac to 0.3.0.
  • crates/uselesskey-jsonwebtoken/README.md
    • Updated description and feature table.
    • Updated example dev-dependencies version to 0.3.
    • Added Claims struct and assertions to the example.
  • crates/uselesskey-jsonwebtoken/src/lib.rs
    • Normalized JWT expiration times in doc examples to 2_000_000_000.
  • crates/uselesskey-jsonwebtoken/tests/jwt_comprehensive.rs
    • Normalized JWT expiration times in tests to 2_000_000_000.
  • crates/uselesskey-jwk/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
  • crates/uselesskey-jwk/README.md
    • Updated description and added 'Example' section.
  • crates/uselesskey-pgp/Cargo.toml
    • Added new crate uselesskey-pgp with version 0.3.0.
    • Added uselesskey-core and pgp dependencies.
  • crates/uselesskey-pgp/README.md
    • Added new README for uselesskey-pgp.
  • crates/uselesskey-pgp/src/keypair.rs
    • Added PgpKeyPair struct and PgpFactoryExt trait for OpenPGP key generation.
    • Implemented methods for accessing private/public key binaries and armored strings, writing to tempfiles, and generating corrupted variants.
    • Added mismatched_public_key_binary and mismatched_public_key_armored methods.
    • Implemented load_inner for key generation and caching.
    • Added spec_to_key_type and build_user_id helper functions.
    • Added comprehensive unit tests for deterministic behavior, random mode, different labels, armored/binary parsing, mismatched keys, user ID sanitization, and corruption helpers.
  • crates/uselesskey-pgp/src/lib.rs
    • Added keypair and spec modules.
    • Exported DOMAIN_PGP_KEYPAIR, PgpFactoryExt, PgpKeyPair, and PgpSpec.
  • crates/uselesskey-pgp/src/spec.rs
    • Added PgpSpec enum for RSA 2048/3072 and Ed25519 OpenPGP keys.
    • Implemented rsa_2048, rsa_3072, ed25519, kind_name, and stable_bytes methods.
    • Added unit tests for stable bytes uniqueness and kind names.
  • crates/uselesskey-ring/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated dependency versions for uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, and uselesskey-core to 0.3.0.
  • crates/uselesskey-ring/README.md
    • Updated description and feature table.
    • Updated example dev-dependencies version to 0.3 and features.
    • Updated example to use keypair variable and assert public modulus length.
  • crates/uselesskey-ring/src/lib.rs
    • Added OnceLock and fx() helper for test setup.
  • crates/uselesskey-ring/tests/ring_comprehensive.rs
    • Updated use statements to be more specific and avoid super::* for external crates.
    • Updated rsa_ring_tests, ecdsa_ring_tests, ed25519_ring_tests, and cross_algorithm_tests modules to import necessary items directly.
  • crates/uselesskey-rsa/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated uselesskey-core and uselesskey-jwk dependency versions to 0.3.0.
  • crates/uselesskey-rsa/README.md
    • Updated description and feature table.
    • Updated example to use private_pem and public_der variables and assert content.
  • crates/uselesskey-rsa/src/keypair.rs
    • Added corrupt_der_deterministic and corrupt_pem_deterministic imports.
    • Added jwk_alg method to determine JWK algorithm based on key bits.
    • Added private_key_pkcs8_pem_corrupt_deterministic and private_key_pkcs8_der_corrupt_deterministic methods.
    • Updated public_jwk and private_key_jwk to use jwk_alg.
  • crates/uselesskey-rsa/tests/rsa_prop.rs
    • Added testutil module and fx() helper.
    • Added assert_private_key_rejects helper function.
    • Updated tests to use fx() instead of Factory::random().
    • Added assertions for bad_pem content in corrupt_pem_fails_to_parse.
    • Added deterministic_corruption_helpers_are_stable test.
    • Updated proptest configurations to use cases: 8 and removed label argument from some tests.
  • crates/uselesskey-rsa/tests/testutil.rs
    • Added new testutil.rs with fx() helper for shared test setup.
  • crates/uselesskey-rustcrypto/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated dependency versions for uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, and uselesskey-hmac to 0.3.0.
  • crates/uselesskey-rustcrypto/README.md
    • Updated description and feature table.
    • Updated example dev-dependencies version to 0.3 and features.
    • Updated example to use signing_key and verifying_key variables and assert signing/verification.
  • crates/uselesskey-rustcrypto/src/lib.rs
    • Added OnceLock and fx() helper for test setup.
  • crates/uselesskey-rustls/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated dependency versions for uselesskey-x509, uselesskey-rsa, uselesskey-ecdsa, and uselesskey-ed25519 to 0.3.0.
  • crates/uselesskey-rustls/README.md
    • Updated description and feature table.
    • Updated example dev-dependencies version to 0.3 and features.
    • Updated example to use svc label and server/client variables.
  • crates/uselesskey-rustls/src/config.rs
    • Modified install_provider to ignore errors when installing the default rustls provider, allowing other providers to be set.
    • Updated tls_handshake_roundtrip and mtls_roundtrip tests to explicitly use ring_provider() instead of relying on install_provider().
  • crates/uselesskey-token/Cargo.toml
    • Added new crate uselesskey-token with version 0.3.0.
    • Added uselesskey-core, base64, rand_core, and serde_json dependencies.
  • crates/uselesskey-token/README.md
    • Added new README for uselesskey-token.
  • crates/uselesskey-token/src/lib.rs
    • Added spec and token modules.
    • Exported TokenSpec, DOMAIN_TOKEN_FIXTURE, TokenFactoryExt, and TokenFixture.
  • crates/uselesskey-token/src/spec.rs
    • Added TokenSpec enum for API key, bearer, and OAuth access tokens.
    • Implemented api_key, bearer, oauth_access_token, kind_name, and stable_bytes methods.
    • Added unit tests for stable bytes uniqueness and kind names.
  • crates/uselesskey-token/src/token.rs
    • Added TokenFixture struct and TokenFactoryExt trait for token generation.
    • Implemented methods for accessing token value and authorization header.
    • Added load_inner for token generation and caching.
    • Implemented generate_token, generate_api_key, generate_bearer_token, generate_oauth_access_token, and random_base62 helper functions.
    • Added comprehensive unit tests for deterministic behavior, random mode, different labels, API key shape, bearer header, OAuth token structure, and debug output.
  • crates/uselesskey-tonic/Cargo.toml
    • Added new crate uselesskey-tonic with version 0.3.0.
    • Added tonic and uselesskey-x509 dependencies.
    • Defined x509 feature.
  • crates/uselesskey-tonic/README.md
    • Added new README for uselesskey-tonic.
  • crates/uselesskey-tonic/src/lib.rs
    • Added TonicIdentityExt, TonicServerTlsExt, TonicClientTlsExt, and TonicMtlsExt traits for tonic TLS integration.
    • Implemented these traits for uselesskey_x509::X509Cert and uselesskey_x509::X509Chain.
    • Added unit tests for self-signed and chain identity/TLS configs, mTLS configs, and deterministic chain material stability.
  • crates/uselesskey-x509/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and documentation fields.
    • Updated uselesskey-core, uselesskey-rsa, and uselesskey-jwk dependency versions to 0.3.0.
  • crates/uselesskey-x509/README.md
    • Updated description and added 'What It Provides' section.
    • Updated feature table description for jwk.
    • Updated example to use Factory::deterministic_from_env and assert PEM content.
  • crates/uselesskey-x509/src/cert.rs
    • Added corrupt_cert_der_deterministic and corrupt_cert_pem_deterministic imports.
    • Added corrupt_cert_pem_deterministic and corrupt_cert_der_deterministic methods.
    • Updated test_self_signed_cert_generation to use fx() helper.
    • Added test_deterministic_corrupt_helpers test.
  • crates/uselesskey-x509/src/chain.rs
    • Added sorted_sans.dedup() for stability in load_chain_inner.
    • Updated test_chain_generation to use fx() helper.
  • crates/uselesskey-x509/src/chain_spec.rs
    • Bumped stable bytes version to 2.
    • Added sorted_sans.dedup() for stability in stable_bytes.
  • crates/uselesskey-x509/src/negative.rs
    • Added corrupt_cert_pem_deterministic and corrupt_cert_der_deterministic functions.
    • Added deterministic_corruption_helpers_are_stable test.
  • crates/uselesskey-x509/src/spec.rs
    • Bumped stable bytes version to 4.
    • Added sorted_sans.dedup() for stability in stable_bytes.
    • Added test_stable_bytes_deduplicates_sans test.
  • crates/uselesskey-x509/tests/x509_unit.rs
    • Updated expected X509Spec stable bytes version prefix to 4.
    • Updated expected ChainSpec stable bytes version prefix to 2.
  • crates/uselesskey/Cargo.toml
    • Bumped version to 0.3.0.
    • Updated description and keywords.
    • Added homepage and badges fields.
    • Updated uselesskey-core, uselesskey-jwk, uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, uselesskey-hmac, and uselesskey-x509 dependency versions to 0.3.0.
    • Added uselesskey-token and uselesskey-pgp dependencies.
    • Added token and pgp features.
    • Updated all-keys and full feature bundles to include pgp and token respectively.
  • crates/uselesskey/examples/negative_fixtures.rs
    • Added cfg(feature = "x509") to main function and imports.
    • Added main function for when x509 feature is not enabled.
  • crates/uselesskey/src/lib.rs
    • Updated documentation to include uselesskey-tonic in adapter crates list.
    • Exported uselesskey_token and uselesskey_pgp modules and their contents.
    • Updated prelude module to include TokenFactoryExt, TokenFixture, TokenSpec, PgpFactoryExt, PgpKeyPair, and PgpSpec.
  • crates/uselesskey/tests/facade.rs
    • Added testutil module and fx() helper.
    • Updated rsa_reexport_works to use testutil::fx().
    • Added token_reexport_works and pgp_reexport_works tests.
  • crates/uselesskey/tests/testutil.rs
    • Added new testutil.rs with fx() helper for shared test setup.
  • docs/architecture.md
    • Added crates/uselesskey-token to the list of microcrates.
    • Added crates/uselesskey-tonic to the list of adapter crates.
    • Updated description of corrupt:* variant to reflect deterministic corruption patterns.
    • Updated Factory extension trait examples to include TokenFactoryExt and fx.x509_self_signed.
  • docs/release.md
    • Updated publish order to include uselesskey-token, uselesskey-pgp, and uselesskey-tonic.
  • docs/roadmap.md
    • Marked 'Deterministic corruption variants', 'Token fixtures', 'OpenPGP fixtures', and 'no_std support in uselesskey-core' as completed.
    • Marked 'uselesskey-tonic' as completed.
    • Removed 'no_std core', 'Deterministic corruptions', 'Token fixtures', and 'PGP key fixtures' from 'Items under evaluation'.
  • tests/Cargo.toml
    • Updated dependency versions for uselesskey-core, uselesskey-rsa, uselesskey-ecdsa, uselesskey-ed25519, uselesskey-hmac, uselesskey-x509, uselesskey-jwk, uselesskey-jsonwebtoken, uselesskey-rustls, uselesskey-ring, and uselesskey-rustcrypto to 0.3.0.
    • Added rustls to e2e feature dependencies.
  • tests/e2e_workflows.rs
    • Added OnceLock for shared fixtures.
    • Introduced shared_chain() and shared_self_signed() to amortize RSA key generation.
    • Updated fx() to install rustls ring provider.
    • Normalized JWT expiration times in tests to 2_000_000_000.
    • Updated TLS workflow tests to use shared fixtures.
  • tests/jwt_integration.rs
    • Normalized JWT expiration times in tests to 2_000_000_000.
  • tests/key_rotation.rs
    • Normalized JWT expiration times in Claims::test() to 2_000_000_000.
    • Updated fx() to install rustls ring provider.
  • tests/testutil.rs
    • Added install_rustls_ring_provider function.
  • tests/tls_integration.rs
    • Added OnceLock for shared fixtures.
    • Introduced shared_chain() and shared_self_signed() to amortize RSA key generation.
    • Updated fx() to install rustls ring provider.
    • Updated TLS configuration tests to use shared fixtures.
  • xtask/src/main.rs
    • Added PathBuf import.
    • Added token and pgp to FEATURE_MATRIX.
    • Modified test() function to exclude uselesskey-bdd.
    • Added --release flag to bdd() and bdd_matrix() commands.
    • Added token, pgp, and tonic to PUBLISH_CRATES list.
    • Updated list_fuzz_targets and count_bdd_scenarios to use workspace_path helper.
    • Added workspace_path helper function.
    • Added CWD_LOCK mutex for list_fuzz_targets and count_bdd_scenarios tests.
  • xtask/src/plan.rs
    • Added uselesskey-token and uselesskey-pgp to uselesskey-core dependents.
    • Added uselesskey-tonic to uselesskey-x509 dependents.
    • Added uselesskey as a dependent for uselesskey-token and uselesskey-pgp.
    • Added uselesskey-tonic to the list of crates with no dependents.
    • Added tests for token_change_expands_to_facade, x509_change_expands_to_tonic_adapter, and pgp_change_expands_to_facade.
Activity
  • The pull request was created by EffortlessSteven.
  • The pull request description provides a detailed summary of the changes, new crates, and improvements.
  • A test plan is outlined, including cargo xtask ci and cargo xtask publish-preflight checks, along with verification steps for GitHub repo settings and crates.io rendering.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread CHANGELOG.md

## [Unreleased]

## [0.3.0] - 2026-02-17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The release date 2026-02-17 is in the future. This appears to be a typo.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/uselesskey-core/src/factory.rs Outdated
Comment on lines +286 to +287
let cached = cache_get(&self.inner.cache, &id)
.expect("uselesskey-core: cached artifact missing after insert");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +116 to +117
let span = chars - 1;
1 + (u16::from_be_bytes([digest[1], digest[2]]) as usize % span)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-token and uselesskey-pgp, plus new adapter crate uselesskey-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.

Comment on lines +112 to +119
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,
});

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 8 to 10
//!
//! Run with: cargo run --example negative_fixtures --features "rsa,x509"

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +84
#[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!(

Copilot AI Feb 18, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

PgpFactoryExt is absent from the Extension Pattern section.

TokenFactoryExt was correctly added at Line 114, but PgpFactoryExt — the parallel trait introduced by uselesskey-pgp in 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-pgp and uselesskey-tonic are 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 | 🔵 Trivial

Migrate remaining Factory::random() calls in ecdsa_tests, ed25519_tests, and hmac_tests to super::fx().

fx() was added to the parent tests module and is already reachable via super::fx() from all three sub-modules, but lines 265, 281, 296, 304, 320, 342, 357, and 371 still call Factory::random(). This leaves the majority of the tests non-deterministic and inconsistent with the test_rsa_sign_verify migration.

♻️ 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 where fx() is defined (tests). Alternatively, move all sub-module tests up one level or re-export fx inside 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 | 🔵 Trivial

Optional: assert "uselesskey-tonic" in core_change_expands_dependents.

A core change transitively reaches uselesskey-tonic via core → 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 | 🔵 Trivial

Consider 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, and mtls_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 | 🟠 Major

Use WebPkiClientVerifier::builder_with_provider() in server_config_mtls_rustls_with_provider.

The provider parameter passed to server_config_mtls_rustls_with_provider is threaded through ServerConfig::builder_with_provider(provider) correctly, but WebPkiClientVerifier::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 provider parameter:

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_both and build_jwks_containing_both have identical bodies — DRY violation.

Unlike build_jwks_all_three which correctly delegates to build_jwks_all, these two share no code despite having identical logic. build_jwks_containing_both should simply call build_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)] on UselessWorld exposes raw private-key material.

Fields pkcs8_der_original, hmac_secret_1, hmac_secret_2, ed25519_pkcs8_der_original, ecdsa_pkcs8_der_original (and their _2 siblings) are Option<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 Debug for these fields, or introduce a Redacted newtype 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

--release for BDD suppresses debug_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 with opt-level=1 and debug-assertions=true would 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 using 2usize..10 to avoid wasting proptest budget.

Since fraction in 1usize..10 already guarantees fraction >= 1, the .max(1) call is dead code. More importantly, when fraction == 1, target_bytes == pem.len() causes prop_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 | 🔵 Trivial

Consider adding a deduplication test for ChainSpec::stable_bytes, parallel to the one added in spec.rs.

spec.rs adds test_stable_bytes_deduplicates_sans; chain_spec.rs only has test_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 var field, and optionally logging message for 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 | 🔵 Trivial

Add a // SAFETY: comment to the unsafe remove_var call in Drop.

Drop can be invoked during thread unwind while other threads are iterating the environment. Even though the variable name is unique, the call still satisfies the unsafe contract 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_configs doesn'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_keys doesn't actually exercise deterministic derivation.

Both init closures return hardcoded values (123u32, 456u32) and ignore _rng. After clear_cache() the closures again return the same hardcoded values, so *a == *a2 is trivially guaranteed regardless of seed, factory mode, or insertion order. The test would pass with Factory::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 | 🔵 Trivial

Remove the unused _id binding in derive_seed_for_test.

_id is created but never read — get_or_init constructs its own ArtifactId internally 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 | 🔵 Trivial

ECDSA and Ed25519 test modules still use Factory::random() instead of fx().

The fx() deterministic helper is defined (lines 126–133) and used by rsa_tests, but ecdsa_tests (lines 171, 189) and ed25519_tests (line 215) still use Factory::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"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +20 to 21
#[cfg(any(not(windows), has_nasm))]
use testutil::fx;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd crates/uselesskey-aws-lc-rs && wc -l tests/aws_lc_rs_comprehensive.rs

Repository: EffortlessMetrics/uselesskey

Length of output: 108


🏁 Script executed:

cd crates/uselesskey-aws-lc-rs && sed -n '1,50p' tests/aws_lc_rs_comprehensive.rs

Repository: 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 -20

Repository: 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 -30

Repository: EffortlessMetrics/uselesskey

Length of output: 1701


🏁 Script executed:

cd crates/uselesskey-aws-lc-rs && rg -n "^mod \w+" tests/aws_lc_rs_comprehensive.rs

Repository: EffortlessMetrics/uselesskey

Length of output: 259


🏁 Script executed:

cd crates/uselesskey-aws-lc-rs && sed -n '28,35p' tests/aws_lc_rs_comprehensive.rs

Repository: EffortlessMetrics/uselesskey

Length of output: 297


🏁 Script executed:

cd crates/uselesskey-aws-lc-rs && rg -B2 "^mod " tests/aws_lc_rs_comprehensive.rs | head -40

Repository: 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 -50

Repository: 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 -50

Repository: 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.

Suggested change
#[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.

Comment on lines +1882 to +1907
#[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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 2

Repository: EffortlessMetrics/uselesskey

Length of output: 14932


🏁 Script executed:

# Check x509_chain structure definition
rg -n "struct.*X509Chain|x509_chain.*:" crates/ -A 5

Repository: EffortlessMetrics/uselesskey

Length of output: 15521


🏁 Script executed:

# Verify pkcs8 crate availability
rg -n "pkcs8" crates/uselesskey-x509/Cargo.toml crates/uselesskey-bdd/Cargo.toml

Repository: 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.

Comment thread crates/uselesskey-bdd/tests/bdd.rs Outdated
Comment on lines +3106 to +3132
#[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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread tests/key_rotation.rs
Comment on lines +31 to +34
fn fx() -> uselesskey_core::Factory {
testutil::install_rustls_ring_provider();
testutil::fx()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment thread tests/tls_integration.rs
Comment on lines 197 to 203
#[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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread xtask/src/main.rs
Comment on lines +872 to +894
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment thread xtask/src/main.rs

#[test]
fn list_fuzz_targets_returns_sorted_rs_stems() {
let _cwd_lock = CWD_LOCK.lock().unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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).

Comment thread xtask/src/plan.rs
Comment on lines +323 to +331
#[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"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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").

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review continued from previous batch...

Comment thread crates/uselesskey-core/src/factory.rs Outdated
- 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Redundant .to_string() on label and variant

ArtifactId::new accepts impl Into<String>, and &str already implements Into<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.

Comment on lines +71 to +73
pub fn corrupt_der_deterministic(der: &[u8], variant: &str) -> Vec<u8> {
let digest = hash32(variant.as_bytes());
let bytes = digest.as_bytes();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +130 to +136
fn reference_truncate_len(len: usize, digest: &[u8; 32]) -> usize {
if len <= 1 {
0
} else {
digest[2] as usize % (len - 1)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Minor: Truncate { bytes } field name is misleading since corrupt_pem operates on chars, not bytes.

Line 81 does pem.chars().take(bytes).collect(), so bytes is 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_again does not update world.x509_chain_spec, unlike gen_x509_chain_second.

After this step, world.x509_chain holds the regenerated chain but world.x509_chain_spec still points to the original gen_x509_chain spec. If a step like add_san_to_chain is later called, it reads from world.x509_chain.spec() (correct), but any future step that reads world.x509_chain_spec directly 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().

Comment on lines +1591 to 1604
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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 by gen_x509_chain, or
  • Renaming chain_root_differs_from_original to chain_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).

Comment on lines +2254 to +2257
#[when("I build a JWKS containing all three keys")]
fn build_jwks_all_three(world: &mut UselessWorld) {
build_jwks_all(world);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment on lines +74 to +83
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,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 -100

Repository: 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 10

Repository: 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).

@EffortlessSteven EffortlessSteven merged commit e7e0e74 into main Feb 18, 2026
3 of 4 checks passed
@EffortlessSteven EffortlessSteven deleted the chore/v0.3.0-release branch February 18, 2026 09:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants